Refactor RSVP system and enhance event management features
- Added markdown support for event descriptions and activity details. - Removed Invite model and adjusted RSVP to use a slug for invites. - Enhanced guest management with real-time updates using websockets. - Improved RSVP page layout and user experience with new button styles. - Added message handling for user notifications. - Updated URL patterns for better clarity and structure.
This commit is contained in:
@@ -11,28 +11,41 @@ Everytime you start: `pipenv shell`
|
||||
|
||||
Check that is works by running `which python`
|
||||
|
||||
# To run
|
||||
Requires Daphne (websockets)
|
||||
`python manage.py runserver`
|
||||
|
||||
## So long as the migrations are not part of the repo
|
||||
`python manage.py makemigrations`
|
||||
`python manage.py migrate`
|
||||
|
||||
## To add a superuser
|
||||
`python manage.py createsuperuser --username <username> --email <email>`
|
||||
|
||||
# Ideas and notes
|
||||
|
||||
## Een
|
||||
|
||||
|
||||
## Twee
|
||||
De invite code onderdeel maken van de RSVP, invite helemaal verwijderen?
|
||||
Een invite juist helemaal loskoppelen, geen connectie meer met RSVP. Alles kopieren.
|
||||
Een invite kan dan ook Guests hebben?
|
||||
|
||||
## Drie
|
||||
Emails! Om te editen, als er comments of wijzigingen zijn!
|
||||
|
||||
## Vier
|
||||
Geen nieuwe gasten toevoegen, als er al een activity geselecteerd is die vol is!
|
||||
## Twee
|
||||
Admin paneel: 4 kolommen op basis van de status: open, incomplete, yes, no
|
||||
Met een websocket.
|
||||
En dat je dan ook live kan zien dat er gasten toegevoegd worden, en als ze dingen veranderen enzo.
|
||||
|
||||
## Vijf
|
||||
## Drie
|
||||
Als iemand geen gasten mag meenemen, die hele stap niet tonen.
|
||||
|
||||
## Vier
|
||||
Activity start en eind tijd klopt niks van hoe het getoond wordt..
|
||||
|
||||
## Vijf
|
||||
Extra gasten als property van een activity, die gewoon getoont worden als groep?
|
||||
|
||||
## Zes
|
||||
Gasten (template): koppelen aan een invite!
|
||||
De buttons tijdens een rsvp renderen serverside en met oob-swapping elke keer terugsturen:
|
||||
- Geen previous op eerste pagina
|
||||
- Geen next, maar finish op laatste pagina
|
||||
- hx-confirm op back-to-event als nog niet voltooid.
|
||||
|
||||
# Things to keep in mind
|
||||
- Move to Postgress
|
||||
|
||||
+4
-4
@@ -2,11 +2,13 @@
|
||||
from django.contrib import admin
|
||||
from events.models import *
|
||||
|
||||
class ActivityAdmin(admin.ModelAdmin):
|
||||
list_display = ('title', 'group', 'event', 'guest_limit', 'remaining_capacity', 'attendee_count', 'attendee_list',)
|
||||
|
||||
admin.site.register(Event)
|
||||
admin.site.register(Activity)
|
||||
admin.site.register(Activity, ActivityAdmin)
|
||||
admin.site.register(ActivityGroup)
|
||||
admin.site.register(ActivitySelection)
|
||||
admin.site.register(Invite)
|
||||
admin.site.register(RSVP)
|
||||
admin.site.register(Guest)
|
||||
admin.site.register(Question)
|
||||
@@ -14,5 +16,3 @@ admin.site.register(QuestionChoice)
|
||||
admin.site.register(Response)
|
||||
admin.site.register(ResponseChoice)
|
||||
admin.site.register(Comment)
|
||||
admin.site.register(GuestListTemplate)
|
||||
admin.site.register(GuestListItem)
|
||||
|
||||
+55
-56
@@ -7,6 +7,7 @@ from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import markdown
|
||||
|
||||
from events.utils.pageutils import is_next_page, is_previous_page
|
||||
|
||||
@@ -58,7 +59,11 @@ class Event(TimestampedModel):
|
||||
return self.title
|
||||
|
||||
@property
|
||||
def image_url(self) -> str:
|
||||
def markdown(self) -> str:
|
||||
return markdown.markdown(self.description)
|
||||
|
||||
@property
|
||||
def static_image_url(self) -> str:
|
||||
image_str = str(self.image)
|
||||
if len(image_str) > 0:
|
||||
return image_str[14:]
|
||||
@@ -66,12 +71,12 @@ class Event(TimestampedModel):
|
||||
|
||||
@property
|
||||
def start_time(self):
|
||||
first = self.activities.order_by('start_time').first()
|
||||
first = self.activities.order_by('start_time').first() # pyright: ignore[reportAttributeAccessIssue]
|
||||
return first.start_time if first else None
|
||||
|
||||
@property
|
||||
def end_time(self):
|
||||
last = self.activities.order_by('-end_time').first()
|
||||
last = self.activities.order_by('-end_time').first() # pyright: ignore[reportAttributeAccessIssue]
|
||||
return last.end_time if last else None
|
||||
|
||||
@property
|
||||
@@ -80,7 +85,7 @@ class Event(TimestampedModel):
|
||||
return expiretime
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
last = self.activities.order_by('-end_time').first()
|
||||
last = self.activities.order_by('-end_time').first() # pyright: ignore[reportAttributeAccessIssue]
|
||||
if not last:
|
||||
return False
|
||||
return timezone.now() >= (last.start_time - self.response_lock_before)
|
||||
@@ -120,6 +125,8 @@ class Activity(TimestampedModel):
|
||||
|
||||
title = models.CharField(max_length=255)
|
||||
description = models.TextField(blank=True)
|
||||
location = models.CharField(max_length=255)
|
||||
|
||||
start_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
@@ -133,12 +140,21 @@ class Activity(TimestampedModel):
|
||||
return f"{self.event.title} — {self.title}"
|
||||
|
||||
@property
|
||||
def image_url(self) -> str:
|
||||
def markdown(self) -> str:
|
||||
return markdown.markdown(self.description)
|
||||
|
||||
def static_image_url(self) -> str:
|
||||
image_str = str(self.image)
|
||||
if len(image_str) > 0:
|
||||
return image_str[14:]
|
||||
return "img/placeholder_activity.jpg"
|
||||
|
||||
def is_locked(self) -> bool:
|
||||
if not self.allow_rsvp:
|
||||
return True
|
||||
|
||||
return timezone.now() >= (self.start_time - self.event.response_lock_before)
|
||||
|
||||
def remaining_capacity(self) -> int:
|
||||
if self.guest_limit is None:
|
||||
return 999
|
||||
@@ -237,19 +253,6 @@ class Activity(TimestampedModel):
|
||||
return guest_list
|
||||
|
||||
|
||||
class Invite(TimestampedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
event = models.ForeignKey(Event, related_name='invites', on_delete=models.CASCADE)
|
||||
code = models.CharField(max_length=64, unique=True, db_index=True)
|
||||
recipient_name = models.CharField(max_length=255, blank=True)
|
||||
recipient_email = models.EmailField(blank=True)
|
||||
personalized_message = models.TextField(blank=True)
|
||||
allow_bring_guests = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"Invite {self.code} -> {self.recipient_name or self.recipient_email}"
|
||||
|
||||
|
||||
class RSVP(TimestampedModel):
|
||||
|
||||
class Page(models.TextChoices):
|
||||
@@ -261,6 +264,7 @@ class RSVP(TimestampedModel):
|
||||
COMPLETED = 'completed', _('Completed')
|
||||
|
||||
class Status(models.TextChoices):
|
||||
UNOPENED = 'unopened', _('Not Opened')
|
||||
NOT_STARTED = 'not_started', _('Not Started')
|
||||
INCOMPLETE = 'incomplete', _('Incomplete')
|
||||
NOT_COMING = 'not_coming', _('Not coming')
|
||||
@@ -268,17 +272,20 @@ class RSVP(TimestampedModel):
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
event = models.ForeignKey(Event, related_name='rsvps', on_delete=models.CASCADE)
|
||||
invite = models.OneToOneField(Invite, null=True, blank=True, related_name='rsvp', on_delete=models.SET_NULL)
|
||||
|
||||
invite = models.SlugField(max_length=255, db_index=True, unique=True)
|
||||
edit_token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
|
||||
responder_name = models.CharField(max_length=255, blank=True)
|
||||
responder_email = models.EmailField(blank=False)
|
||||
send_email_updates = models.BooleanField(default=True)
|
||||
|
||||
personalized_message = models.TextField(blank=True)
|
||||
allow_bring_guests = models.BooleanField(default=False)
|
||||
|
||||
page = models.CharField(max_length=16, choices=Page.choices, default=Page.EMAIL)
|
||||
furthest_page = models.CharField(max_length=16, choices=Page.choices, default=Page.EMAIL)
|
||||
status = models.CharField(max_length=16, choices=Status.choices, default=Status.NOT_STARTED)
|
||||
status = models.CharField(max_length=16, choices=Status.choices, default=Status.UNOPENED)
|
||||
|
||||
notes = models.TextField(blank=True)
|
||||
|
||||
@@ -288,10 +295,10 @@ class RSVP(TimestampedModel):
|
||||
def __str__(self):
|
||||
return f"RSVP {self.edit_token} — {self.event.title} — {self.responder_email} — {self.status}"
|
||||
|
||||
def guest_count(self) -> int:
|
||||
return self.guests.count()
|
||||
|
||||
@property
|
||||
def guest_count(self) -> int:
|
||||
return self.guests.count() # pyright: ignore[reportAttributeAccessIssue]
|
||||
|
||||
def selected_activities_ids(self):
|
||||
selected_activities = (
|
||||
ActivitySelection.objects
|
||||
@@ -300,6 +307,17 @@ class RSVP(TimestampedModel):
|
||||
selected_activities_ids = [act.activity.id for act in selected_activities]
|
||||
return selected_activities_ids
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
main_guest = self.guests.filter(is_main_invitee=True).first() # pyright: ignore[reportAttributeAccessIssue]
|
||||
if(main_guest):
|
||||
return main_guest.name
|
||||
|
||||
if(self.responder_name):
|
||||
return self.responder_name
|
||||
|
||||
return "friend"
|
||||
|
||||
PAGE_ORDER = [
|
||||
'email',
|
||||
'guests',
|
||||
@@ -326,6 +344,7 @@ class RSVP(TimestampedModel):
|
||||
self.furthest_page = new_page
|
||||
|
||||
if self.status == self.Status.NOT_STARTED:
|
||||
#TODO send an email here
|
||||
self.status = self.Status.INCOMPLETE
|
||||
|
||||
self.page = new_page
|
||||
@@ -400,7 +419,7 @@ class RSVP(TimestampedModel):
|
||||
return f'Please answer required question: "{q.text}"'
|
||||
|
||||
elif q.scope == "guest":
|
||||
for guest in self.guests.all():
|
||||
for guest in self.guests.all(): # pyright: ignore[reportAttributeAccessIssue]
|
||||
resp = Response.objects.filter(guest=guest, question=q).first()
|
||||
if not resp:
|
||||
return f'Please answer required question "{q.text}" for all guests.'
|
||||
@@ -450,7 +469,6 @@ class Guest(TimestampedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
event = models.ForeignKey(Event, related_name='guests', on_delete=models.CASCADE)
|
||||
rsvp = models.ForeignKey(RSVP, related_name='guests', on_delete=models.CASCADE)
|
||||
invite = models.ForeignKey(Invite, null=True, blank=True, related_name='guests', on_delete=models.SET_NULL)
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
age_group = models.CharField(max_length=10, choices=AGE_GROUP, default='adult')
|
||||
@@ -459,6 +477,15 @@ class Guest(TimestampedModel):
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.age_group})"
|
||||
|
||||
@property
|
||||
def firstname(self) -> str:
|
||||
return self.name.split(" ")[0]
|
||||
|
||||
@property
|
||||
def lastname(self) -> str:
|
||||
return self.name.split(" ")[1:]
|
||||
|
||||
|
||||
class Question(TimestampedModel):
|
||||
KIND_CHOICES = [
|
||||
@@ -573,38 +600,10 @@ class ResponseChoice(TimestampedModel):
|
||||
|
||||
class Comment(TimestampedModel):
|
||||
event = models.ForeignKey(Event, related_name='comments', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
guest = models.ForeignKey(Guest, related_name='comments', on_delete=models.CASCADE)
|
||||
|
||||
text = models.TextField()
|
||||
notify_subscribers = models.BooleanField(default=False)
|
||||
|
||||
def __str__(self):
|
||||
return f"Comment by {self.name} on {self.event}"
|
||||
|
||||
|
||||
class GuestListTemplate(TimestampedModel):
|
||||
owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
title = models.CharField(max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
class GuestListItem(TimestampedModel):
|
||||
template = models.ForeignKey(GuestListTemplate, related_name='items', on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=255)
|
||||
email = models.EmailField(blank=True)
|
||||
is_main_invitee = models.BooleanField(default=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class GuestItemInvite(TimestampedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
guest = models.ForeignKey(GuestListItem, related_name="guest_invite", on_delete=models.CASCADE)
|
||||
invite = models.ForeignKey(Invite, related_name="guest_invite", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
unique_together = (("guest", "invite"),)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.guest} added to {self.invite}"
|
||||
return f"Comment by {self.guest.name} on {self.event.title}"
|
||||
+64
-3
@@ -1,9 +1,8 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from asgiref.sync import async_to_sync, sync_to_async
|
||||
from asgiref.sync import async_to_sync
|
||||
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.db.models.signals import post_save, post_delete, pre_delete
|
||||
from django.dispatch import receiver
|
||||
from channels.layers import get_channel_layer
|
||||
from events.models import RSVP, Activity, Event, ActivitySelection, Guest, Response
|
||||
@@ -14,6 +13,7 @@ logger = logging.getLogger(__name__)
|
||||
###
|
||||
# Guests
|
||||
###
|
||||
|
||||
@receiver(post_delete, sender=Guest)
|
||||
def guest_deleted(sender, instance, **kwargs):
|
||||
"""Delete question responses for this guest"""
|
||||
@@ -21,9 +21,70 @@ def guest_deleted(sender, instance, **kwargs):
|
||||
|
||||
logger.debug(f"Signal {guest}")
|
||||
|
||||
guest_delete_responses(guest)
|
||||
guest_notify_activities(guest)
|
||||
|
||||
def guest_delete_responses(guest:Guest):
|
||||
# Remove all responses from the guest that is deleted
|
||||
Response.objects.filter(guest=guest).delete()
|
||||
|
||||
def guest_notify_activities(guest:Guest):
|
||||
"""Broadcast activity selection changes to all connected clients."""
|
||||
rsvp:RSVP = guest.rsvp
|
||||
event:Event = rsvp.event
|
||||
|
||||
# Get all activities with this RSVP
|
||||
activities = rsvp.activities
|
||||
|
||||
for activity in activities.all():
|
||||
logger.debug(f"Signal {activity}")
|
||||
|
||||
capacity = activity.remaining_capacity()
|
||||
if capacity is None:
|
||||
capacity = 100
|
||||
|
||||
activity_json = {
|
||||
"activity": str(activity.id),
|
||||
"rsvp": str(rsvp.id),
|
||||
"action": "select",
|
||||
"attendees": activity.attendee_count(),
|
||||
"capacity": capacity,
|
||||
"guests": activity.attendee_list()
|
||||
}
|
||||
|
||||
# Send message to the group
|
||||
async_to_sync(send_channel)(event.id, activity_json)
|
||||
|
||||
@receiver(post_save, sender=Guest)
|
||||
def activity_guests_changed(sender, instance, created, **kwargs):
|
||||
"""Broadcast activity selection changes to all connected clients."""
|
||||
guest:Guest = instance
|
||||
rsvp:RSVP = guest.rsvp
|
||||
event:Event = rsvp.event
|
||||
|
||||
logger.debug(f"Guest added: {guest.name}")
|
||||
|
||||
# Get all activities with this RSVP
|
||||
activities = rsvp.activities
|
||||
|
||||
for activity in activities.all():
|
||||
logger.debug(f"Signal {activity}")
|
||||
|
||||
capacity = activity.remaining_capacity()
|
||||
if capacity is None:
|
||||
capacity = 100
|
||||
|
||||
activity_json = {
|
||||
"activity": str(activity.id),
|
||||
"rsvp": str(rsvp.id),
|
||||
"action": "select",
|
||||
"attendees": activity.attendee_count(),
|
||||
"capacity": capacity,
|
||||
"guests": activity.attendee_list()
|
||||
}
|
||||
|
||||
# Send message to the group
|
||||
async_to_sync(send_channel)(event.id, activity_json)
|
||||
|
||||
###
|
||||
# Activity Selections
|
||||
|
||||
@@ -181,12 +181,12 @@ button:hover, .button:hover, a.button:hover, input[type="submit"]:hover, input[t
|
||||
}
|
||||
|
||||
.bg-gray {
|
||||
background: gray !important;
|
||||
background: darkgray !important;
|
||||
}
|
||||
|
||||
.bd-gray {
|
||||
border: 2px solid transparent;
|
||||
border-color: gray !important;
|
||||
border-color: darkgray !important;
|
||||
}
|
||||
|
||||
.locked {
|
||||
@@ -263,6 +263,17 @@ button {
|
||||
margin: 0.25rem;
|
||||
}
|
||||
|
||||
.button-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.button-row-right {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* media layout: text left, image right, image height matches text column */
|
||||
.subcard .media-row {
|
||||
display: flex;
|
||||
@@ -311,4 +322,8 @@ button {
|
||||
.group-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
@@ -98,19 +98,29 @@ function activitiesPage() {
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
const capacity = $('#activity-' + data.activity + ' .numcap');
|
||||
const fullwarn = $('#activity-' + data.activity + ' .full-warning');
|
||||
if(capacity !== null && data.capacity !== null){
|
||||
capacity.html(data.capacity);
|
||||
|
||||
if(data.capacity == 0){
|
||||
fullwarn.removeClass("hidden");
|
||||
} else {
|
||||
fullwarn.addClass("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Activity capacity (when there is a guest limit)
|
||||
const activity = $('#activity-' + data.activity);
|
||||
const spacewarn = $('#activity-' + data.activity + ' .space-warning');
|
||||
if(activity !== null && data.capacity !== null){
|
||||
if(!(activity.hasClass("attending"))){
|
||||
if(data.capacity >= guestCount){
|
||||
activity.removeClass("bg-gray bd-gray");
|
||||
spacewarn.addClass("hidden");
|
||||
}
|
||||
else{
|
||||
activity.addClass("bg-gray bd-gray");
|
||||
spacewarn.removeClass("hidden");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
<body hx-headers='{"X-CSRFTOKEN": "{{ csrf_token }}"}' hx-ext="ws">
|
||||
|
||||
<main>
|
||||
{% if messages %}
|
||||
<div class="messages">
|
||||
{% for message in messages %}
|
||||
<div class="card {% if message.tags %}{{ message.tags }}{% endif %}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
{% if event.description %}
|
||||
<div class="subcard">
|
||||
{{ event.description }}
|
||||
{{ event.markdown|safe }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
<div class="card">
|
||||
{% load static %}
|
||||
<img src="{% static event.image_url %}">
|
||||
<img src="{% static event.static_image_url %}">
|
||||
</div>
|
||||
|
||||
{% if response %}
|
||||
<div class="card {% if response.joining %}bg-green{% elif response.not_coming %}bg-red{% endif %}">
|
||||
<h3>Hello {{ response.rsvp.responder_name }}!</h3>
|
||||
<div class="card {% if response.joining %}bg-green{% elif response.not_coming %}bg-red{% elif response.in_progress %}bg-orange{% endif %}">
|
||||
<h3>Hello {{ response.rsvp.name }}!</h3>
|
||||
{% if response.not_started %}
|
||||
|
||||
You have been invited! Please let us know if you are coming!
|
||||
@@ -41,13 +41,13 @@
|
||||
|
||||
Sad that you are not coming?
|
||||
</br></br>
|
||||
<a href="{% url 'events:rsvp-restart' response.rsvp.id %}" class="button bg-green hover">I changed my mind!</a>
|
||||
<a href="{% url 'events:rsvp-restart' response.rsvp.id %}" class="button bg-accent hover">I changed my mind!</a>
|
||||
|
||||
{% elif response.joining %}
|
||||
|
||||
You have responded!
|
||||
</br></br>
|
||||
<a href="{% url 'events:rsvp-update' response.rsvp.id %}" class="button bg-green hover">Update response!</a>
|
||||
<a href="{% url 'events:rsvp-update' response.rsvp.id %}" class="button bg-accent hover">Update response!</a>
|
||||
<a href="{% url 'events:rsvp-decline' response.rsvp.id %}" class="button bg-red hover">I can no longer come</a>
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -1,38 +1,58 @@
|
||||
<div class="subcard {% if response and activity.id in response.rsvp.selected_activities_ids %}bg-green attending{% endif %}" id="activity-{{activity.id}}">
|
||||
<div class="media-row">
|
||||
<div class="media-content">
|
||||
<h3>{{ activity.title }}</h3>
|
||||
<h3>{{ activity.title }} {% if activity.remaining_capacity <= 0 %}<span class="badge bg-orange">FULL</span>{% endif %}{% if activity.is_locked %}<span class="badge bg-red">LOCKED</span>{% endif %}</h3>
|
||||
|
||||
<p>
|
||||
{{ activity.start_time|date:"DATETIME_FORMAT" }}
|
||||
{% if activity.end_time %}
|
||||
– {{ activity.end_time|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div class="subcard {% if response and activity.id in response.rsvp.selected_activities_ids %}bg-green attending{% endif %}">
|
||||
<h4>📖 About</h4>
|
||||
<p>{{ activity.markdown|safe }}</p>
|
||||
</div>
|
||||
|
||||
{% if activity.guest_limit %}
|
||||
<p>
|
||||
Places remaining:
|
||||
<span class="numcap">{{ activity.remaining_capacity }}</span> / {{ activity.guest_limit }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
People going:
|
||||
<span class="numatt">{{ activity.attendee_count }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="subcard {% if response and activity.id in response.rsvp.selected_activities_ids %}bg-green attending{% endif %}">
|
||||
<h4>🕙 When</h4>
|
||||
<p>
|
||||
{{ activity.start_time|date:"DATETIME_FORMAT" }}
|
||||
{% if activity.end_time %}
|
||||
– {{ activity.end_time|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3>Attendees:</h3>
|
||||
<span class="stratt">
|
||||
{% for guest_list in activity.attendee_list %}
|
||||
<span class="badge bg-blue">
|
||||
{{ guest_list|join:", " }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
<div class="subcard {% if response and activity.id in response.rsvp.selected_activities_ids %}bg-green attending{% endif %}">
|
||||
<h4>📍Where</h4>
|
||||
<p>
|
||||
{{ activity.location }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="subcard {% if response and activity.id in response.rsvp.selected_activities_ids %}bg-green attending{% endif %}">
|
||||
<h4>👥Capacity</h4>
|
||||
{% if activity.guest_limit %}
|
||||
<p>
|
||||
Places remaining:
|
||||
<span class="numcap">{{ activity.remaining_capacity }}</span> / {{ activity.guest_limit }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
People going:
|
||||
<span class="numatt">{{ activity.attendee_count }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="subcard {% if response and activity.id in response.rsvp.selected_activities_ids %}bg-green attending{% endif %}">
|
||||
<h4>🚶➡️Attendees:</h4>
|
||||
<span class="stratt">
|
||||
{% for guest_list in activity.attendee_list %}
|
||||
<span class="badge bg-blue">
|
||||
{{ guest_list|join:", " }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% load static %}
|
||||
<img class="thumbnail" src="{% static activity.image_url %}" alt="{{ activity.title }} image">
|
||||
<img class="thumbnail" src="{% static activity.static_image_url %}" alt="{{ activity.title }} image">
|
||||
</div>
|
||||
</div>
|
||||
@@ -13,8 +13,8 @@
|
||||
{% else %}
|
||||
|
||||
<h2 class="group-title">
|
||||
Hello {{ invite.recipient_name }}
|
||||
{% if invite.allow_bring_guests %}
|
||||
Hello {{ rsvp.name }}
|
||||
{% if rsvp.allow_bring_guests %}
|
||||
<span class="badge bg-green">Allowed to bring guests</span>
|
||||
{% endif %}
|
||||
</h2>
|
||||
@@ -24,18 +24,27 @@
|
||||
hx-trigger="load">
|
||||
</div>
|
||||
|
||||
<a class="button bg-orange hover"
|
||||
hx-get="{% url 'events:rsvp-page-change' rsvp.id "previous" %}"
|
||||
hx-target="#rsvp-flow"
|
||||
>Previous</a>
|
||||
<div class="button-row">
|
||||
|
||||
<a class="button bg-blue hover"
|
||||
hx-get="{% url 'events:rsvp-page-change' rsvp.id "next" %}"
|
||||
hx-target="#rsvp-flow"
|
||||
>Next</a>
|
||||
<a class="button bg-orange hover"
|
||||
hx-get="{% url 'events:rsvp-page-change' rsvp.id "previous" %}"
|
||||
hx-target="#rsvp-flow"
|
||||
>Previous</a>
|
||||
|
||||
<a class="button bg-red hover"
|
||||
href="{% url 'events:rsvp-decline' rsvp.id %}" >Not coming</a>
|
||||
<a class="button bg-blue hover"
|
||||
hx-get="{% url 'events:rsvp-page-change' rsvp.id "next" %}"
|
||||
hx-target="#rsvp-flow"
|
||||
>Next</a>
|
||||
|
||||
<div class="button-row-right">
|
||||
|
||||
<a class="button bg-red hover"
|
||||
href="{% url 'events:rsvp-decline' rsvp.id %}" >Not coming</a>
|
||||
|
||||
<a class="button bg-green hover"
|
||||
href="{% url 'events:event' event.slug %}" >Back to event</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
{% if not activity.is_locked %}
|
||||
<div class="subcard cursor {% if activity.id in selected_activities %}bg-green attending{% elif activity.remaining_capacity < rsvp.guest_count %}bg-gray bd-gray{% endif %}"
|
||||
hx-post="{% url 'events:rsvp-post-activity' rsvp.id activity.id %}"
|
||||
hx-target="#rsvp-activity"
|
||||
@@ -5,33 +6,46 @@
|
||||
|
||||
id="activity-{{activity.id}}"
|
||||
>
|
||||
<h3>{{ activity.title }}</h3>
|
||||
<h3>{{ activity.title }} <span class="full-warning badge bg-orange {% if activity.remaining_capacity > 0 %}hidden{% endif %}">FULL</span></h3>
|
||||
|
||||
<p>
|
||||
🕙
|
||||
{{ activity.start_time|date:"DATETIME_FORMAT" }}
|
||||
{% if activity.end_time %}
|
||||
– {{ activity.end_time|date:"TIME_FORMAT" }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
📍{{ activity.location }}
|
||||
</p>
|
||||
|
||||
{% if activity.guest_limit %}
|
||||
<p>
|
||||
Places remaining:
|
||||
<span class="numcap">{{ activity.remaining_capacity }}</span> / {{ activity.guest_limit }}
|
||||
👥Places remaining:
|
||||
<span class="numcap">{{ activity.remaining_capacity }}</span> / {{ activity.guest_limit }}
|
||||
<span class="space-warning badge bg-red {% if activity.id in selected_activities or activity.remaining_capacity >= rsvp.guest_count %}hidden{% endif %}">Not enough space for {{rsvp.guest_count}} people</span>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
People going:
|
||||
👥People going:
|
||||
<span class="numatt">{{ activity.attendee_count }}</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h3>Attendees:</h3>
|
||||
<span class="stratt">
|
||||
{% for guest_list in activity.attendee_list %}
|
||||
<span class="badge bg-blue">
|
||||
{{ guest_list|join:", " }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% if activity.attendee_list|length == 0 %}
|
||||
<p>🚶➡️No-one is coming yet</p>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="subcard {% if activity.id in selected_activities %}bg-green attending{% endif %}">
|
||||
<h3>{{ activity.title }} <span class="badge bg-red">LOCKED</span></h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -1,21 +1,20 @@
|
||||
<div class="card" id="rsvp-invite">
|
||||
<div class="card" id="rsvp-email">
|
||||
|
||||
<h3>You are invited to {{ event.title }}</h3>
|
||||
<p> {{ event.description }} </p>
|
||||
|
||||
<div class="subcard">
|
||||
<h4>Message from the host</h4>
|
||||
<p> {{ invite.personalized_message }} </p>
|
||||
<p> {{ rsvp.personalized_message }} </p>
|
||||
</div>
|
||||
|
||||
<div class="subcard">
|
||||
<h4>Email updates</h4>
|
||||
<span class="badge bg-green">Your email will never be shared with other people</span>
|
||||
<span class="badge bg-gray">Your email will never be shared with other people</span>
|
||||
<p>
|
||||
You will always receive an email with a link to edit your response, and when a selected activity changes.
|
||||
Optional emails are comments from other people.
|
||||
</p>
|
||||
<form method="post" hx-post="{% url 'events:rsvp-post-email' rsvp.id %}" hx-target="#post-result">
|
||||
<form method="post" hx-post="{% url 'events:rsvp-post-email' rsvp.id %}" hx-target="#post-result" style="margin-top:1em;">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<button type="submit"><div id="post-result">Save</div></button>
|
||||
|
||||
@@ -3,18 +3,26 @@
|
||||
|
||||
{% for guest in guests %}
|
||||
<div class="subcard">
|
||||
{{ guest.name }} <span class="badge">{{ guest.age_group }}</span>
|
||||
<div class="button-row">
|
||||
{{ guest.name }} <span class="badge">{{ guest.age_group }}</span>
|
||||
|
||||
<button
|
||||
hx-delete="{% url 'events:rsvp-delete-guest' rsvp.id guest.id %}"
|
||||
hx-target="#rsvp-guest"
|
||||
hx-swap="outerHTML"
|
||||
>Delete</button>
|
||||
<div class="button-row-right">
|
||||
{% if not guest.is_main_invitee %}
|
||||
|
||||
<button class="bg-red"
|
||||
hx-delete="{% url 'events:rsvp-delete-guest' rsvp.id guest.id %}"
|
||||
hx-target="#rsvp-guest"
|
||||
hx-swap="outerHTML"
|
||||
>Delete</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if space_for_extra_guest %}
|
||||
<div class="subcard" id="rsvp-guest-add">
|
||||
<span class="badge bg-orange">Last names are not shown to other people</span>
|
||||
<h4>Add guest <span class="badge bg-orange">Last names are not shown to other people</span></h4>
|
||||
<form method="post" hx-post="{% url 'events:rsvp-post-guest' rsvp.id %}"
|
||||
hx-target="#rsvp-guest"
|
||||
hx-swap="outerHTML"
|
||||
@@ -24,4 +32,10 @@
|
||||
<button type="submit">Add guest</button>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="subcard bg-red">
|
||||
<h4>Add guest</h4>
|
||||
<p>There is no space left in some of your selected activities.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -10,4 +10,10 @@
|
||||
{% for question in questions %}
|
||||
{% include "rsvp/rsvp_page_questions_detail.html" %}
|
||||
{% endfor %}
|
||||
|
||||
{% if questions|length <= 0 %}
|
||||
<p>
|
||||
There are no questions to answer.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
+5
-2
@@ -7,12 +7,15 @@ from . import views
|
||||
app_name = "events"
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
# Main event page
|
||||
path("events/<slug:slug>/", views.event, name="event"),
|
||||
|
||||
path("invite/<slug:slug>/rsvp/<str:code>/", views.invite_redirect, name="invite"),
|
||||
# Invite URL, can only be used once
|
||||
path("invite/<slug:slug>", views.invite_redirect, name="invite"),
|
||||
|
||||
# RSVP Form
|
||||
path("events/<slug:slug>/rsvp/<uuid:code>/page/<str:page>", views.rsvp_base_page, name="rsvp-base"),
|
||||
path("rsvp/<uuid:code>/page/<str:page>", views.rsvp_base_page, name="rsvp-base"),
|
||||
path("htmx/rsvp/<str:id>/page/<str:page>", views.rsvp_pages, name="rsvp-pages"),
|
||||
|
||||
path(
|
||||
|
||||
@@ -1,20 +1,7 @@
|
||||
import json
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from ..models import RSVP, Activity, ActivitySelection, Event, Invite
|
||||
|
||||
def create_rsvp(event: Event, invite: Invite):
|
||||
rsvp = RSVP(event=event, invite=invite)
|
||||
|
||||
if(invite.recipient_name != ""):
|
||||
rsvp.responder_name = invite.recipient_name
|
||||
|
||||
if(invite.recipient_email != ""):
|
||||
rsvp.responder_email = invite.recipient_email
|
||||
|
||||
rsvp.save()
|
||||
|
||||
return rsvp
|
||||
from ..models import RSVP, Activity, ActivitySelection, Event
|
||||
|
||||
def create_or_switch_activity_response(rsvp: RSVP, activity: Activity):
|
||||
"""
|
||||
@@ -40,7 +27,7 @@ def create_or_switch_activity_response(rsvp: RSVP, activity: Activity):
|
||||
# Hardcoded, because why not
|
||||
remaining_capacity = 100
|
||||
|
||||
if rsvp.guest_count() > remaining_capacity:
|
||||
if rsvp.guest_count > remaining_capacity:
|
||||
return
|
||||
|
||||
# Step 3: Check if new selection is illegal (choosing one in same group)
|
||||
@@ -68,8 +55,7 @@ def create_or_switch_activity_response(rsvp: RSVP, activity: Activity):
|
||||
def create_context(rsvp:RSVP, error:str="") -> dict:
|
||||
context = {
|
||||
"rsvp": rsvp,
|
||||
"event": rsvp.event,
|
||||
"invite": rsvp.invite
|
||||
"event": rsvp.event
|
||||
}
|
||||
|
||||
if(error != ""):
|
||||
@@ -90,14 +76,6 @@ def get_rsvp(context:dict) -> RSVP:
|
||||
|
||||
return rsvp
|
||||
|
||||
def get_invite(context:dict) -> Invite:
|
||||
invite = get_from_context(context, "invite")
|
||||
|
||||
if not invite:
|
||||
raise KeyError
|
||||
|
||||
return invite
|
||||
|
||||
def get_event(context:dict) -> Event:
|
||||
event = get_from_context(context, "event")
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ def summary_guests(rsvp:RSVP) -> str:
|
||||
summary = f"Guests: let us know who will come!"
|
||||
|
||||
if rsvp.is_page_finished(RSVP.Page.GUESTS):
|
||||
guests = rsvp.guest_count()
|
||||
guests = rsvp.guest_count
|
||||
if guests == 0:
|
||||
summary = f"Guests: Don't forget to add yourself!"
|
||||
if guests == 1:
|
||||
summary = f"Guests: Just you."
|
||||
if guests > 1:
|
||||
summary = f"Guests: You are bringing {guests - 1} guests."
|
||||
summary = f"Guests: You are coming with {guests} people."
|
||||
|
||||
return summary
|
||||
|
||||
@@ -31,7 +31,7 @@ def summary_activities(rsvp:RSVP) -> str:
|
||||
available_activities = Activity.objects.filter(event=rsvp.event)
|
||||
total_activities_count = len(available_activities)
|
||||
activities_with_space_count = sum(
|
||||
1 for a in available_activities if a.remaining_capacity() >= rsvp.guest_count()
|
||||
1 for a in available_activities if a.remaining_capacity() >= rsvp.guest_count
|
||||
)
|
||||
|
||||
if rsvp.is_page_finished(RSVP.Page.ACTIVITIES) and chosen_activities_count > 0:
|
||||
|
||||
+22
-16
@@ -8,18 +8,18 @@ from django.db import transaction
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
|
||||
from events.utils.cookieutils import set_rsvp_cookie
|
||||
from events.utils.summary import summary_activities, summary_email, summary_guests, summary_notes, summary_questions
|
||||
|
||||
from ..utils.rsvputils import add_to_context, create_or_switch_activity_response, create_context, get_event, get_from_context, get_rsvp, create_rsvp
|
||||
from ..utils.rsvputils import add_to_context, create_or_switch_activity_response, create_context, get_event, get_from_context, get_rsvp
|
||||
|
||||
from ..forms import GuestForm, NotesForm, RSVPForm
|
||||
|
||||
from ..models import (
|
||||
Event,
|
||||
Guest,
|
||||
Invite,
|
||||
Activity,
|
||||
ActivityGroup,
|
||||
RSVP,
|
||||
@@ -40,7 +40,7 @@ def rsvp_start(request, id:str):
|
||||
if not (rsvp.status == RSVP.Status.NOT_STARTED or rsvp.status == RSVP.Status.INCOMPLETE):
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
response = redirect(reverse('events:rsvp-base', args=[rsvp.event.slug, rsvp.edit_token, rsvp.page]))
|
||||
response = redirect(reverse('events:rsvp-base', args=[rsvp.edit_token, rsvp.page]))
|
||||
return response
|
||||
|
||||
# Update a completed RSVP
|
||||
@@ -53,7 +53,7 @@ def rsvp_update(request, id:str):
|
||||
rsvp.page = RSVP.Page.ACTIVITIES
|
||||
rsvp.save()
|
||||
|
||||
response = redirect(reverse('events:rsvp-base', args=[rsvp.event.slug, rsvp.edit_token, rsvp.page]))
|
||||
response = redirect(reverse('events:rsvp-base', args=[rsvp.edit_token, rsvp.page]))
|
||||
return response
|
||||
|
||||
# Stop an existing RSVP
|
||||
@@ -81,26 +81,25 @@ def rsvp_restart(request, id:str):
|
||||
rsvp.status = RSVP.Status.INCOMPLETE
|
||||
rsvp.save()
|
||||
|
||||
response = redirect(reverse('events:rsvp-base', args=[rsvp.event.slug, rsvp.edit_token, rsvp.page]))
|
||||
response = redirect(reverse('events:rsvp-base', args=[rsvp.edit_token, rsvp.page]))
|
||||
return response
|
||||
|
||||
|
||||
|
||||
# Main entrypoint for rsvp page
|
||||
def rsvp_base_page(request, slug:str, code:str, page:str):
|
||||
event = get_object_or_404(Event, slug=slug)
|
||||
def rsvp_base_page(request, code:str, page:str):
|
||||
rsvp = get_object_or_404(RSVP, edit_token=code)
|
||||
|
||||
context = {
|
||||
"rsvp": rsvp,
|
||||
"event": event,
|
||||
"event": rsvp.event,
|
||||
"invite": rsvp.invite,
|
||||
"page": page,
|
||||
"is_locked": event.is_locked()
|
||||
"is_locked": rsvp.event.is_locked()
|
||||
}
|
||||
|
||||
response = render(request, "rsvp/rsvp.html", context)
|
||||
set_rsvp_cookie(response, event, rsvp)
|
||||
set_rsvp_cookie(response, rsvp.event, rsvp)
|
||||
return response
|
||||
|
||||
|
||||
@@ -122,11 +121,9 @@ def rsvp_pages(request, id:str, page:str, error:str=""):
|
||||
"page_questions": _render_page_questions(request, context, page),
|
||||
"page_notes": _render_page_notes(request, context, page)
|
||||
}
|
||||
|
||||
# Add `hx-push` header and set to value of `reverse('events:rsvp-base', args=[slug, rsvp.edit_token, rsvp.page])`
|
||||
|
||||
|
||||
response = render(request, "rsvp/rsvp_pages.html", rendercontent)
|
||||
response["HX-Push-Url"] = reverse('events:rsvp-base', args=[get_event(context).slug, rsvp.edit_token, page])
|
||||
response["HX-Push-Url"] = reverse('events:rsvp-base', args=[rsvp.edit_token, page])
|
||||
return response
|
||||
|
||||
# Email page
|
||||
@@ -159,7 +156,16 @@ def _render_page_guest(request, context, page) -> str:
|
||||
)
|
||||
|
||||
add_to_context(context, "guests", guests)
|
||||
add_to_context(context, "form", GuestForm())
|
||||
add_to_context(context, "form", GuestForm())
|
||||
|
||||
selected_activities = (
|
||||
ActivitySelection.objects
|
||||
.filter(rsvp=get_rsvp(context))
|
||||
)
|
||||
|
||||
# Check if any of the activities is full
|
||||
selected_activities_full = [(True if act.activity.remaining_capacity() < 1 else False) for act in selected_activities]
|
||||
add_to_context(context, "space_for_extra_guest", True not in selected_activities_full)
|
||||
|
||||
template = get_template("rsvp/rsvp_page_guest.html")
|
||||
return template.render(context=context, request=request)
|
||||
@@ -299,7 +305,7 @@ def page_change(request, id:str, page:str):
|
||||
|
||||
# Done with rsvp
|
||||
if rsvp.page == RSVP.Page.COMPLETED:
|
||||
#response = redirect(reverse('events:event', args=[rsvp.event.slug]))
|
||||
messages.success(request, "Thank you for responding!")
|
||||
response = HttpResponse()
|
||||
response["HX-Redirect"] = reverse('events:event', args=[rsvp.event.slug])
|
||||
return response
|
||||
|
||||
+18
-28
@@ -1,16 +1,16 @@
|
||||
# views.py
|
||||
from email import message
|
||||
import logging
|
||||
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.http import Http404, HttpResponseBadRequest, HttpRequest
|
||||
from django.http import HttpRequest
|
||||
from django.urls import reverse
|
||||
from django.contrib import messages
|
||||
|
||||
from events.utils.cookieutils import get_rsvp_from_cookie, set_rsvp_cookie
|
||||
from events.utils.rsvputils import create_rsvp
|
||||
|
||||
from ..models import (
|
||||
Event,
|
||||
Invite,
|
||||
Activity,
|
||||
RSVP,
|
||||
)
|
||||
@@ -18,36 +18,28 @@ from ..models import (
|
||||
# Logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def invite_redirect(request:HttpRequest, slug, code):
|
||||
def invite_redirect(request:HttpRequest, slug:str):
|
||||
"""
|
||||
Redirect from the unique invite page
|
||||
to the rsvp response page which is linked to the response
|
||||
"""
|
||||
|
||||
event = get_object_or_404(Event, slug=slug)
|
||||
response = redirect(reverse('events:event', args=[slug]))
|
||||
rsvp = get_object_or_404(RSVP, invite=slug)
|
||||
response = redirect(reverse('events:event', args=[rsvp.event.slug]))
|
||||
|
||||
logger.info(f"Invite `{code}` opened for Event `{slug}`")
|
||||
# Opened for the first time
|
||||
#TODO what is email not sent out?
|
||||
if(rsvp.status == RSVP.Status.UNOPENED):
|
||||
rsvp.status = RSVP.Status.NOT_STARTED
|
||||
rsvp.save()
|
||||
|
||||
# Only set the cookie the first time, after this they need to use the invite link
|
||||
set_rsvp_cookie(response, rsvp.event, rsvp)
|
||||
else:
|
||||
messages.warning(request, "You can only use the invite once, please use the link in the email.")
|
||||
|
||||
rsvp:RSVP
|
||||
logger.info(f"Invite `{slug}` opened for Event `{rsvp.event.title}`")
|
||||
|
||||
try:
|
||||
invite = get_object_or_404(Invite, code=code)
|
||||
|
||||
# The invite code does not match the event
|
||||
if invite.event.id != event.id:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
rsvp = RSVP.objects.get(invite=invite)
|
||||
set_rsvp_cookie(response, event, rsvp)
|
||||
except Http404 as e:
|
||||
logger.debug(str(e))
|
||||
pass
|
||||
except RSVP.DoesNotExist as e:
|
||||
logger.debug(str(e))
|
||||
rsvp = create_rsvp(event, invite)
|
||||
set_rsvp_cookie(response, event, rsvp)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@@ -71,8 +63,6 @@ def event(request:HttpRequest, slug):
|
||||
else:
|
||||
ungrouped.append(activity)
|
||||
|
||||
|
||||
|
||||
context = {
|
||||
"event": event,
|
||||
"groups": groups,
|
||||
@@ -88,7 +78,7 @@ def event(request:HttpRequest, slug):
|
||||
joining = True if rsvp.status == RSVP.Status.YES_JOINING else False
|
||||
not_coming = True if rsvp.status == RSVP.Status.NOT_COMING else False
|
||||
in_progress = True if rsvp.status == RSVP.Status.INCOMPLETE else False
|
||||
not_started = True if rsvp.status == RSVP.Status.NOT_STARTED else False
|
||||
not_started = True if rsvp.status == RSVP.Status.NOT_STARTED or rsvp.status == RSVP.Status.UNOPENED else False
|
||||
|
||||
response = {
|
||||
"rsvp": rsvp,
|
||||
|
||||
@@ -138,6 +138,14 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
},
|
||||
]
|
||||
|
||||
from django.contrib.messages import constants as message_constants
|
||||
|
||||
MESSAGE_TAGS = {
|
||||
message_constants.INFO: "bg-blue",
|
||||
message_constants.SUCCESS: "bg-green",
|
||||
message_constants.WARNING: "bg-orange",
|
||||
message_constants.ERROR: "bg-red",
|
||||
}
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
||||
|
||||
+1
-1
@@ -18,6 +18,6 @@ from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('django-admin/', admin.site.urls),
|
||||
path('', include('events.urls', namespace='events')),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user