class Category(models.Model, metaclass=ModelTranslateMeta): """Describes a Message category""" # These should be the keys of the categories that we automatically created # in the migrations (0012 to be specific) GENERAL = "general" PIZZA = "pizza" EVENT = "event" NEWSLETTER = "newsletter" PARTNER = "partner" PHOTO = "photo" BOARD = "board" key = models.CharField(max_length=16, primary_key=True) name = MultilingualField( models.CharField, _("name"), max_length=32, ) description = MultilingualField(models.TextField, _("description"), default="") def __str__(self): return self.name_en
class MerchandiseItem(models.Model, metaclass=ModelTranslateMeta): """ Merchandise items This model describes merchandise items. """ #: Name of the merchandise item. name = MultilingualField(models.CharField, max_length=200) #: Price of the merchandise item price = models.DecimalField(max_digits=5, decimal_places=2) #: Description of the merchandise item description = MultilingualField(models.TextField) #: Image of the merchandise item image = models.ImageField(upload_to="public/merchandise") def __str__(self): """Gives the name of the merchandise item in the currently active locale. :return: The name of the merchandise item. :rtype: str """ return str(self.name)
class NewsletterContent(models.Model, metaclass=ModelTranslateMeta): """Describes one piece of basic content of a newsletter""" title = MultilingualField( models.CharField, max_length=150, verbose_name=_("Title"), blank=False, null=False, ) url = models.URLField( verbose_name=_("URL"), blank=True, null=True, help_text=_("If filled, it will make the title a link to this URL"), ) description = MultilingualField( HTMLField, verbose_name=_("Description"), blank=False, null=False, ) newsletter = models.ForeignKey(Newsletter, on_delete=models.CASCADE) order = models.PositiveIntegerField(verbose_name=_("order"), blank=False, null=True, default=0) def clean(self): super().clean() errors = {} url = "admin/newsletters/" if url in self.description_nl: errors.update({ "description_nl": _("Please make sure all urls are absolute " "and start with http(s)://.") }) if url in self.description_en: errors.update({ "description_en": _("Please make sure all urls are absolute " "and start with http(s)://.") }) if errors: raise ValidationError(errors) def __str__(self): return self.title class Meta: ordering = ("order", )
class PartnerEvent(models.Model, metaclass=ModelTranslateMeta): """Model describing partner event.""" partner = models.ForeignKey( Partner, verbose_name=_("partner"), on_delete=models.CASCADE, related_name="events", blank=True, null=True, ) other_partner = models.CharField(max_length=255, blank=True) title = MultilingualField(models.CharField, _("title"), max_length=100) description = MultilingualField(models.TextField, _("description")) location = MultilingualField( models.CharField, _("location"), max_length=255, ) start = models.DateTimeField(_("start time")) end = models.DateTimeField(_("end time")) url = models.URLField(_("website")) published = models.BooleanField(_("published"), default=False) def clean(self): """Validate the partner event.""" super().clean() errors = {} if (not self.partner and not self.other_partner) or (self.partner and self.other_partner): errors.update({ "partner": _("Please select or enter " "a partner for this event."), "other_partner": _("Please select or enter " "a partner for this event."), }) if errors: raise ValidationError(errors) def __str__(self): """Return the event title.""" return self.title
class Document(models.Model, metaclass=ModelTranslateMeta): """Describes a base document""" class Meta: verbose_name = _("Document") verbose_name_plural = _("Documents") DOCUMENT_CATEGORIES = ( ("annual", _("Annual document")), ("association", _("Association document")), ("event", _("Event document")), ("minutes", _("Minutes")), ("misc", _("Miscellaneous document")), ) name = MultilingualField(models.CharField, verbose_name=_("name"), max_length=200) created = models.DateTimeField( verbose_name=_("created"), auto_now_add=True, ) last_updated = models.DateTimeField(verbose_name=_("last updated"), auto_now=True) category = models.CharField( max_length=40, choices=DOCUMENT_CATEGORIES, verbose_name=_("category"), default="misc", ) file = MultilingualField( models.FileField, verbose_name=_("file"), upload_to="documents/", validators=[ FileExtensionValidator(["txt", "pdf", "jpg", "jpeg", "png"]) ], ) members_only = models.BooleanField(verbose_name=_("members only"), default=False) def get_absolute_url(self): return reverse("documents:document", kwargs={"pk": self.pk}) def __str__(self): return "%s (%s)" % (self.name, str(self.created.date()))
class Course(models.Model, metaclass=ModelTranslateMeta): """Describes a course""" name = MultilingualField(models.CharField, max_length=255) categories = models.ManyToManyField(Category, verbose_name=_("categories"), blank=True) old_courses = models.ManyToManyField("self", symmetrical=False, verbose_name=_("old courses"), blank=True) course_code = models.CharField(max_length=16) ec = models.IntegerField(verbose_name=_("EC")) since = models.IntegerField() until = models.IntegerField(blank=True, null=True) def __str__(self): return "{} ({})".format(self.name, self.course_code) def get_absolute_url(self): return reverse("education:course", args=[str(self.pk)]) class Meta: ordering = ["-pk"] verbose_name = _("course") verbose_name_plural = _("courses")
class FrontpageArticle(models.Model, metaclass=ModelTranslateMeta): """Front page articles""" title = MultilingualField( models.CharField, verbose_name=_("Title"), help_text=_("The title of the article; what goes in the header"), blank=False, max_length=80, ) content = MultilingualField( HTMLField, verbose_name=_("Content"), help_text=_("The content of the article; what text to display."), blank=False, max_length=5000, ) since = models.DateTimeField( verbose_name=_("Display since"), help_text=_("Hide this article before this time."), default=timezone.now, ) until = models.DateTimeField( verbose_name=_("Display until"), help_text=_("Hide this article after this time."), blank=True, null=True, ) class Meta: ordering = ("-since", ) def __str__(self): return self.title @property def is_visible(self): """Is this announcement currently visible""" return (self.until is None or self.until > timezone.now()) and ( self.since is None or self.since <= timezone.now())
class NewsletterEvent(NewsletterContent): """Describes one piece of event content of a newsletter""" where = MultilingualField( models.CharField, max_length=150, verbose_name=_("Where"), blank=False, null=False, ) start_datetime = models.DateTimeField( verbose_name=_("Start date and time"), blank=False, null=False, ) end_datetime = models.DateTimeField( verbose_name=_("End date and time"), blank=False, null=False, ) show_costs_warning = models.BooleanField( verbose_name=_("Show warnings about costs"), default=True ) price = models.DecimalField( verbose_name=_("Price (in Euro)"), max_digits=5, decimal_places=2, blank=True, null=True, default=None, ) penalty_costs = models.DecimalField( verbose_name=_("Costs (in Euro)"), max_digits=5, decimal_places=2, blank=True, null=True, default=None, help_text=_( "This is the price that a member has to pay when he/she did not show up." ), ) def clean(self): """Make sure that the event end date is after the start date""" super().clean() if ( self.end_datetime is not None and self.start_datetime is not None and self.end_datetime < self.start_datetime ): raise ValidationError( {"end_datetime": _("Can't have an event travel back in time")} )
class VacancyCategory(models.Model, metaclass=ModelTranslateMeta): """Model describing vacancy categories.""" name = MultilingualField(models.CharField, max_length=30) slug = models.SlugField() def __str__(self): """Return the category name.""" return str(self.name) class Meta: """Meta class for vacancy category model.""" verbose_name_plural = _("Vacancy Categories")
class Category(models.Model, metaclass=ModelTranslateMeta): """Describes a course category""" name = MultilingualField( models.CharField, max_length=64, ) def __str__(self): return self.name def get_absolute_url(self): return reverse("education:category", args=[str(self.pk)]) class Meta: verbose_name = _("category") verbose_name_plural = _("categories")
class Announcement(models.Model, metaclass=ModelTranslateMeta): """Describes an announcement""" content = MultilingualField( HTMLField, verbose_name=_("Content"), help_text=_("The content of the announcement; what text to display."), blank=False, max_length=500, ) since = models.DateTimeField( verbose_name=_("Display since"), help_text=_("Hide this announcement before this time."), default=timezone.now, ) until = models.DateTimeField( verbose_name=_("Display until"), help_text=_("Hide this announcement after this time."), blank=True, null=True, ) icon = models.CharField( verbose_name=_("Font Awesome icon"), help_text=_("Font Awesome abbreviation for icon to use."), max_length=150, default="bullhorn", ) closeable = models.BooleanField(default=True) class Meta: ordering = ("-since", ) def __str__(self): return self.content @property def is_visible(self): """Is this announcement currently visible""" return (self.until is None or self.until > timezone.now()) and ( self.since is None or self.since <= timezone.now())
class GeneralMeeting(models.Model, metaclass=ModelTranslateMeta): """Describes a general meeting""" class Meta: verbose_name = _("General meeting") verbose_name_plural = _("General meetings") ordering = ["datetime"] documents = models.ManyToManyField( Document, verbose_name=_("documents"), blank=True, ) datetime = models.DateTimeField(verbose_name=_("datetime"), ) location = MultilingualField(models.CharField, verbose_name=_("location"), max_length=200) def __str__(self): return timezone.localtime(self.datetime).strftime("%Y-%m-%d")
class Product(models.Model, metaclass=ModelTranslateMeta): """Describes a product""" objects = models.Manager() available_products = AvailableProductManager() name = models.CharField(max_length=50) description = MultilingualField(models.TextField) price = models.DecimalField(max_digits=5, decimal_places=2) available = models.BooleanField(default=True) restricted = models.BooleanField( default=False, help_text=_("Only allow to be ordered by people with the " "'order restricted products' permission."), ) def __str__(self): return self.name class Meta: ordering = ("name", ) permissions = (("order_restricted_products", _("Order restricted products")), )
class Message(models.Model, metaclass=ModelTranslateMeta): """Describes a push notification""" objects = NormalMessageManager() all_objects = MessageManager() users = models.ManyToManyField(settings.AUTH_USER_MODEL) title = MultilingualField(models.CharField, max_length=150, verbose_name=_("title")) body = MultilingualField(models.TextField, verbose_name=_("body")) url = models.CharField( verbose_name=_("url"), max_length=256, null=True, blank=True, ) category = models.ForeignKey( Category, on_delete=models.CASCADE, verbose_name=_("category"), default="general", ) sent = models.DateTimeField( verbose_name=_("sent"), null=True, ) failure = models.IntegerField( verbose_name=_("failure"), blank=True, null=True, ) success = models.IntegerField( verbose_name=_("success"), blank=True, null=True, ) def __str__(self): return "{}: {}".format(self.title, self.body) def send(self, **kwargs): if self: success_total = 0 failure_total = 0 ttl = kwargs.get("ttl", 3600) for lang in settings.LANGUAGES: with override(lang[0]): reg_ids = list( Device.objects.filter( user__in=self.users.all(), receive_category__key=self.category_id, active=True, language=lang[0], ).values_list("registration_id", flat=True)) data = kwargs.get("data", {}) if self.url is not None: data["url"] = self.url data["title"] = self.title data["body"] = str(self.body) message = messaging.Message( notification=messaging.Notification( title=data["title"], body=data["body"], ), data=data, android=messaging.AndroidConfig( ttl=datetime.timedelta(seconds=ttl), priority="normal", notification=messaging.AndroidNotification( color="#E62272", sound="default", ), ), ) for reg_id in reg_ids: message.token = reg_id try: messaging.send(message, dry_run=kwargs.get( "dry_run", False)) success_total += 1 except messaging.UnregisteredError: failure_total += 1 Device.objects.filter( registration_id=reg_id).delete() except exceptions.InvalidArgumentError: failure_total += 1 Device.objects.filter( registration_id=reg_id).update(active=False) except exceptions.FirebaseError: failure_total += 1 self.sent = timezone.now() self.success = success_total self.failure = failure_total self.save()
class MemberGroupMembership(models.Model, metaclass=ModelTranslateMeta): """Describes a group membership""" objects = models.Manager() active_objects = ActiveMembershipManager() member = models.ForeignKey( "members.Member", on_delete=models.CASCADE, verbose_name=_("Member"), ) group = models.ForeignKey( MemberGroup, on_delete=models.CASCADE, verbose_name=_("Group"), ) since = models.DateField( verbose_name=_("Member since"), help_text=_("The date this member joined in this role"), default=datetime.date.today, ) until = models.DateField( verbose_name=_("Member until"), help_text=_("A member until this time " "(can't be in the future)."), blank=True, null=True, ) chair = models.BooleanField( verbose_name=_("Chair of the group"), help_text=_("There can only be one chair at a time!"), default=False, ) role = MultilingualField( models.CharField, _("role"), help_text=_("The role of this member"), max_length=255, blank=True, null=True, ) @property def initial_connected_membership(self): """Find the oldest membership directly connected to the current one""" qs = MemberGroupMembership.objects.filter( group=self.group, member=self.member, until__lte=self.since, until__gte=self.since - datetime.timedelta(days=1), ) if qs.count() >= 1: # should only be one; should be unique return qs.first().initial_connected_membership else: return self @property def latest_connected_membership(self): """ Find the newest membership directly connected to the current one (thus the membership that started at the moment the current one ended) """ if self.until: qs = MemberGroupMembership.objects.filter( group=self.group, member=self.member, since__lte=self.until, since__gte=self.until + datetime.timedelta(days=1), ) if qs.count() >= 1: # should only be one; should be unique return qs.last().latest_connected_membership return self @property def is_active(self): """Is this membership currently active""" return self.until is None or self.until > timezone.now().date() def clean(self): try: if self.until and (not self.since or self.until < self.since): raise ValidationError( {"until": _("End date can't be before start date")}) if self.until and self.until > timezone.now().date(): raise ValidationError( {"until": _("End date can't be in the future")}) if self.since and self.group.since and self.since < self.group.since: raise ValidationError({ "since": _("Start date can't be before group start date") }) if self.since and self.group.until and self.since > self.group.until: raise ValidationError( {"since": _("Start date can't be after group end date")}) except MemberGroupMembership.group.RelatedObjectDoesNotExist: return False def validate_unique(self, *args, **kwargs): try: super().validate_unique(*args, **kwargs) # Check if a group has more than one chair if self.chair: chairs = MemberGroupMembership.objects.filter(group=self.group, chair=True) for chair in chairs: if chair.pk == self.pk: continue if ((chair.until is None and (self.until is None or self.until > chair.since)) or (self.until is None and self.since < chair.until) or (self.until and chair.until and self.since < chair.until and self.until > chair.since)): raise ValidationError({ NON_FIELD_ERRORS: _("There already is a " "chair for this time period") }) # check if this member is already in the group in this period memberships = MemberGroupMembership.objects.filter( group=self.group, member=self.member) for mship in memberships: if mship.pk == self.pk: continue if ((mship.until is None and (self.until is None or self.until > mship.since)) or (self.until is None and self.since < mship.until) or (self.until and mship.until and self.since < mship.until and self.until > mship.since)): raise ValidationError({ "member": _("This member is already in the group for " "this period") }) except ( MemberGroupMembership.member.RelatedObjectDoesNotExist, MemberGroupMembership.group.RelatedObjectDoesNotExist, ): return False def save(self, *args, **kwargs): super().save(*args, **kwargs) self.member.is_staff = (self.member.membergroupmembership_set.exclude( until__lte=timezone.now().date()).count()) >= 1 self.member.save() def __str__(self): return _("{member} membership of {group} " "since {since}, until {until}").format(member=self.member, group=self.group, since=self.since, until=self.until) class Meta: verbose_name = _("group membership") verbose_name_plural = _("group memberships")
class MemberGroup(models.Model, metaclass=ModelTranslateMeta): """Describes a groups of members""" objects = models.Manager() active_objects = ActiveMemberGroupManager() name = MultilingualField( models.CharField, max_length=40, verbose_name=_("Name"), unique=True, ) description = MultilingualField( HTMLField, verbose_name=_("Description"), ) photo = models.ImageField( verbose_name=_("Image"), upload_to="public/committeephotos/", null=True, blank=True, ) members = models.ManyToManyField( "members.Member", through="activemembers.MemberGroupMembership") permissions = models.ManyToManyField( Permission, verbose_name=_("permissions"), blank=True, ) since = models.DateField( _("founded in"), null=True, blank=True, ) until = models.DateField( _("existed until"), null=True, blank=True, ) active = models.BooleanField( default=False, help_text=_("This should only be unchecked if the committee has been " "dissolved. The websites assumes that any committees on it" " existed at some point."), ) contact_email = models.EmailField( _("contact email address"), blank=True, null=True, ) contact_mailinglist = models.OneToOneField( "mailinglists.MailingList", verbose_name=_("contact mailing list"), null=True, blank=True, on_delete=models.SET_NULL, ) display_members = models.BooleanField(default=False, ) @property def contact_address(self): if self.contact_mailinglist: return f"{self.contact_mailinglist.name}@{settings.SITE_DOMAIN}" return self.contact_email def clean(self): if (self.contact_email is not None and self.contact_mailinglist is not None) or (self.contact_email is None and self.contact_mailinglist is None): raise ValidationError({ "contact_email": _("Please use either the mailing list " "or email address option."), "contact_mailinglist": _("Please use either the mailing list " "or email address option."), }) def __str__(self): return self.name def get_absolute_url(self): try: return self.board.get_absolute_url() except self.DoesNotExist: try: return self.committee.get_absolute_url() except self.DoesNotExist: try: return self.society.get_absolute_url() except self.DoesNotExist: pass class Meta: verbose_name = _("member group") verbose_name_plural = _("member groups")
class Newsletter(models.Model, metaclass=ModelTranslateMeta): """Describes a newsletter""" title = MultilingualField( models.CharField, max_length=150, verbose_name=_("Title"), help_text=_("The title is used for the email subject."), blank=False, ) date = models.DateField( verbose_name=_("Date"), help_text=_( "This date is used to extract the week of this " "newsletter, best scenario:" "always use the monday of the week the newsletter is " "for. If you leave it empty no week is shown." ), blank=True, null=True, ) send_date = models.DateTimeField( verbose_name=_("Send date"), blank=True, null=True, ) description = MultilingualField( HTMLField, verbose_name=_("Introduction"), help_text=_( "This is the text that starts the newsletter. It always " 'begins with "Dear members" and you can append ' "whatever you want." ), blank=False, ) sent = models.BooleanField(default=False) def get_absolute_url(self): return reverse("newsletters:preview", args=(self.pk,)) def clean(self): super().clean() errors = {} url = "admin/newsletters/" if url in self.description_en: errors.update( { "description_en": _( "Please make sure all urls are absolute " "and contain http(s)://." ) } ) if self.send_date and self.send_date <= timezone.now(): errors.update( {"send_date": _("Please make sure the send date is not in the past.")} ) if errors: raise ValidationError(errors) class Meta: permissions = (("send_newsletter", "Can send newsletter"),) def __str__(self): return str(self.title)
class Album(models.Model, metaclass=ModelTranslateMeta): title = MultilingualField( models.CharField, _("title"), max_length=200, ) dirname = models.CharField( verbose_name=_("directory name"), max_length=200, ) date = models.DateField(verbose_name=_("date"), ) slug = models.SlugField( verbose_name=_("slug"), unique=True, ) hidden = models.BooleanField(verbose_name=_("hidden"), default=False) new_album_notification = models.ForeignKey( ScheduledMessage, on_delete=models.deletion.SET_NULL, blank=True, null=True) _cover = models.OneToOneField( Photo, on_delete=models.SET_NULL, blank=True, null=True, related_name="covered_album", verbose_name=_("cover image"), ) shareable = models.BooleanField(verbose_name=_("shareable"), default=False) photosdir = "photos" photospath = os.path.join(settings.MEDIA_ROOT, photosdir) @cached_property def cover(self): cover = None if self._cover is not None: return self._cover elif self.photo_set.exists(): random.seed(self.dirname) cover = random.choice(self.photo_set.all()) return cover def __str__(self): return "{} {}".format(self.date.strftime("%Y-%m-%d"), self.title) def get_absolute_url(self): return reverse("photos:album", args=[str(self.slug)]) def save(self, *args, **kwargs): # dirname is only set for new objects, to avoid ever changing it if self.pk is None: self.dirname = self.slug if not self.hidden and (self.new_album_notification is None or not self.new_album_notification.sent): new_album_notification_time = timezone.now() + timezone.timedelta( hours=1) new_album_notification = ScheduledMessage() if (self.new_album_notification is not None and not self.new_album_notification.sent): new_album_notification = self.new_album_notification new_album_notification.title_en = "New album uploaded" new_album_notification.title_nl = "Nieuw album geüpload" new_album_notification.body_en = ("A new photo album '{}' has " "just been uploaded".format( self.title_en)) new_album_notification.body_nl = ("Een nieuw fotoalbum '{}' is " "zojuist geüpload".format( self.title_nl)) new_album_notification.category = Category.objects.get( key=Category.PHOTO) new_album_notification.url = (f"{settings.BASE_URL}" f"{self.get_absolute_url()}") new_album_notification.time = new_album_notification_time new_album_notification.save() self.new_album_notification = new_album_notification self.new_album_notification.users.set(Member.current_members.all()) elif (self.hidden and self.new_album_notification is not None and not self.new_album_notification.sent): existing_notification = self.new_album_notification self.new_album_notification = None existing_notification.delete() super().save(*args, **kwargs) @property def access_token(self): return hashlib.sha256("{}album{}".format( settings.SECRET_KEY, self.pk).encode("utf-8")).hexdigest() class Meta: ordering = ("-date", "title_en")
class _TestItem5(models.Model, metaclass=ModelTranslateMeta): foreign = MultilingualField(field_type, "TestItem5")
class _TestItem2(models.Model, metaclass=ModelTranslateMeta): text = MultilingualField(models.TextField, verbose_name="Text")
class _TestItem3b(models.Model, metaclass=ModelTranslateMeta): text = MultilingualField(models.TextField)
class RegistrationInformationField(models.Model, metaclass=ModelTranslateMeta): """Describes a field description to ask for when registering""" BOOLEAN_FIELD = "boolean" INTEGER_FIELD = "integer" TEXT_FIELD = "text" FIELD_TYPES = ( (BOOLEAN_FIELD, _("Checkbox")), (TEXT_FIELD, _("Text")), (INTEGER_FIELD, _("Integer")), ) event = models.ForeignKey(Event, models.CASCADE) type = models.CharField( _("field type"), choices=FIELD_TYPES, max_length=10, ) name = MultilingualField( models.CharField, _("field name"), max_length=100, ) description = MultilingualField( models.TextField, _("description"), null=True, blank=True, ) required = models.BooleanField(_("required"), ) def get_value_for(self, registration): if self.type == self.TEXT_FIELD: value_set = self.textregistrationinformation_set elif self.type == self.BOOLEAN_FIELD: value_set = self.booleanregistrationinformation_set elif self.type == self.INTEGER_FIELD: value_set = self.integerregistrationinformation_set try: return value_set.get(registration=registration).value except ( TextRegistrationInformation.DoesNotExist, BooleanRegistrationInformation.DoesNotExist, IntegerRegistrationInformation.DoesNotExist, ): return None def set_value_for(self, registration, value): if self.type == self.TEXT_FIELD: value_set = self.textregistrationinformation_set elif self.type == self.BOOLEAN_FIELD: value_set = self.booleanregistrationinformation_set elif self.type == self.INTEGER_FIELD: value_set = self.integerregistrationinformation_set try: field_value = value_set.get(registration=registration) except BooleanRegistrationInformation.DoesNotExist: field_value = BooleanRegistrationInformation() except TextRegistrationInformation.DoesNotExist: field_value = TextRegistrationInformation() except IntegerRegistrationInformation.DoesNotExist: field_value = IntegerRegistrationInformation() field_value.registration = registration field_value.field = self field_value.value = value field_value.save() def __str__(self): return "{} ({})".format(self.name, dict(self.FIELD_TYPES)[self.type]) class Meta: order_with_respect_to = "event"
class _TestItem4(models.Model, metaclass=ModelTranslateMeta): text = MultilingualField(models.CharField, "Text", max_length=100)
class Event(models.Model, metaclass=ModelTranslateMeta): """Describes an event""" CATEGORY_ALUMNI = "alumni" CATEGORY_EDUCATION = "education" CATEGORY_CAREER = "career" CATEGORY_LEISURE = "leisure" CATEGORY_ASSOCIATION = "association" CATEGORY_OTHER = "other" EVENT_CATEGORIES = ( (CATEGORY_ALUMNI, _("Alumni")), (CATEGORY_EDUCATION, _("Education")), (CATEGORY_CAREER, _("Career")), (CATEGORY_LEISURE, _("Leisure")), (CATEGORY_ASSOCIATION, _("Association Affairs")), (CATEGORY_OTHER, _("Other")), ) DEFAULT_NO_REGISTRATION_MESSAGE = _("No registration required / " "Geen aanmelding vereist") title = MultilingualField(models.CharField, _("title"), max_length=100) description = MultilingualField( HTMLField, _("description"), help_text=_("Please fill in both of the description boxes (EN/NL)," " even if your event is Dutch only! Fill in the English " "description in Dutch then."), ) start = models.DateTimeField(_("start time")) end = models.DateTimeField(_("end time")) organiser = models.ForeignKey("activemembers.MemberGroup", models.PROTECT, verbose_name=_("organiser")) category = models.CharField( max_length=40, choices=EVENT_CATEGORIES, verbose_name=_("category"), help_text=_("Alumni: Events organised for alumni, " "Education: Education focused events, " "Career: Career focused events, " "Leisure: borrels, parties, game activities etc., " "Association Affairs: general meetings or " "any other board related events, " "Other: anything else."), ) registration_start = models.DateTimeField( _("registration start"), null=True, blank=True, help_text=_("If you set a registration period registration will be " "required. If you don't set one, registration won't be " "required. Prefer times when people don't have lectures, " "e.g. 12:30 instead of 13:37."), ) registration_end = models.DateTimeField( _("registration end"), null=True, blank=True, help_text=_("If you set a registration period registration will be " "required. If you don't set one, registration won't be " "required."), ) cancel_deadline = models.DateTimeField(_("cancel deadline"), null=True, blank=True) send_cancel_email = models.BooleanField( _("send cancellation notifications"), default=True, help_text=_("Send an email to the organising party when a member " "cancels their registration after the deadline."), ) location = MultilingualField( models.CharField, _("location"), max_length=255, ) map_location = models.CharField( _("location for minimap"), max_length=255, help_text=_("Location of Huygens: Heyendaalseweg 135, Nijmegen. " "Location of Mercator 1: Toernooiveld 212, Nijmegen. " "Not shown as text!!"), ) price = models.DecimalField( _("price"), max_digits=5, decimal_places=2, default=0, validators=[validators.MinValueValidator(0)], ) fine = models.DecimalField( _("fine"), max_digits=5, decimal_places=2, default=0, # Minimum fine is checked in this model's clean(), as it is only for # events that require registration. help_text=_("Fine if participant does not show up (at least €5)."), validators=[validators.MinValueValidator(0)], ) max_participants = models.PositiveSmallIntegerField( _("maximum number of participants"), blank=True, null=True, ) no_registration_message = MultilingualField( models.CharField, _("message when there is no registration"), max_length=200, blank=True, null=True, help_text=(format_lazy("{} {}", _("Default:"), DEFAULT_NO_REGISTRATION_MESSAGE)), ) published = models.BooleanField(_("published"), default=False) registration_reminder = models.ForeignKey( ScheduledMessage, on_delete=models.deletion.SET_NULL, related_name="registration_event", blank=True, null=True, ) start_reminder = models.ForeignKey( ScheduledMessage, on_delete=models.deletion.SET_NULL, related_name="start_event", blank=True, null=True, ) documents = models.ManyToManyField( "documents.Document", verbose_name=_("documents"), blank=True, ) slide = models.ForeignKey( Slide, verbose_name="slide", help_text=_("Change the header-image on the event's info-page to one " "specific to this event."), blank=True, on_delete=models.deletion.SET_NULL, null=True, ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._price = self.price self._registration_start = self.registration_start @property def after_cancel_deadline(self): return self.cancel_deadline and self.cancel_deadline <= timezone.now() @property def registration_started(self): return self.registration_start <= timezone.now() @property def registration_required(self): return bool(self.registration_start) or bool(self.registration_end) def has_fields(self): return self.registrationinformationfield_set.count() > 0 def reached_participants_limit(self): """Is this event up to capacity?""" return (self.max_participants is not None and self.max_participants <= self.eventregistration_set.filter(date_cancelled=None).count()) @property def registrations(self): """Queryset with all non-cancelled registrations""" return self.eventregistration_set.filter(date_cancelled=None) @property def participants(self): """Return the active participants""" if self.max_participants is not None: return self.registrations.order_by("date")[:self.max_participants] return self.registrations.order_by("date") @property def queue(self): """Return the waiting queue""" if self.max_participants is not None: return self.registrations.order_by("date")[self.max_participants:] return [] @property def cancellations(self): """Return a queryset with the cancelled events""" return self.eventregistration_set.exclude( date_cancelled=None).order_by("date_cancelled") @property def registration_allowed(self): now = timezone.now() return (bool(self.registration_start or self.registration_end) and self.registration_end > now >= self.registration_start) @property def cancellation_allowed(self): now = timezone.now() return (bool(self.registration_start or self.registration_end) and self.registration_start <= now < self.start) def is_pizza_event(self): try: self.pizzaevent return True except ObjectDoesNotExist: return False def clean(self): super().clean() errors = {} if self.start is None: errors.update( {"start": _("Start cannot have an empty date or time field")}) if self.end is None: errors.update( {"end": _("End cannot have an empty date or time field")}) if self.start is not None and self.end is not None: if self.end < self.start: errors.update( {"end": _("Can't have an event travel back in time")}) if self.registration_required: if self.fine < 5: errors.update({ "fine": _("The fine for this event is too low " "(must be at least €5).") }) for lang in settings.LANGUAGES: field = "no_registration_message_" + lang[0] if getattr(self, field): errors.update({ field: _("Doesn't make sense to have this " "if you require registrations.") }) if not self.registration_start: errors.update({ "registration_start": _("If registration is required, you need a start of " "registration") }) if not self.registration_end: errors.update({ "registration_end": _("If registration is required, you need an end of " "registration") }) if not self.cancel_deadline: errors.update({ "cancel_deadline": _("If registration is required, " "you need a deadline for the cancellation") }) elif self.cancel_deadline > self.start: errors.update({ "cancel_deadline": _("The cancel deadline should be" " before the start of the event.") }) if (self.registration_start and self.registration_end and (self.registration_start >= self.registration_end)): message = _("Registration start should be before " "registration end") errors.update({ "registration_start": message, "registration_end": message }) try: if (self.organiser is not None and self.send_cancel_email and self.organiser.contact_mailinglist is None): errors.update({ "send_cancel_email": _("This organiser does not " "have a contact mailinglist.") }) except ObjectDoesNotExist: pass if self.published: if (self.price != self._price and self._registration_start and self._registration_start <= timezone.now()): errors.update({ "price": _("You cannot change this field after " "the registration has started.") }) if (self._registration_start and self.registration_start != self._registration_start and self._registration_start <= timezone.now()): errors.update({ "registration_start": _("You cannot change this field after " "the registration has started.") }) if errors: raise ValidationError(errors) def get_absolute_url(self): return reverse("events:event", args=[str(self.pk)]) def save(self, *args, **kwargs): delete_collector = Collector( using=router.db_for_write(self.__class__, instance=self)) if not self.pk: super().save(*args, **kwargs) if self.published: if self.registration_required: registration_reminder_time = (self.registration_start - timezone.timedelta(hours=1)) registration_reminder = ScheduledMessage() if (self.registration_reminder is not None and not self.registration_reminder.sent): registration_reminder = self.registration_reminder if registration_reminder_time > timezone.now(): registration_reminder.title_en = "Event registration" registration_reminder.title_nl = "Evenement registratie" registration_reminder.body_en = ("Registration for '{}' " "starts in 1 hour".format( self.title_en)) registration_reminder.body_nl = ("Registratie voor '{}' " "start in 1 uur".format( self.title_nl)) registration_reminder.category = Category.objects.get( key=Category.EVENT) registration_reminder.time = registration_reminder_time registration_reminder.url = ( f"{settings.BASE_URL}" f'{reverse("events:event", args=[self.id])}') registration_reminder.save() self.registration_reminder = registration_reminder self.registration_reminder.users.set( Member.current_members.all()) elif registration_reminder.pk is not None: delete_collector.add([self.registration_reminder]) self.registration_reminder = None start_reminder_time = self.start - timezone.timedelta(hours=1) start_reminder = ScheduledMessage() if self.start_reminder is not None and not self.start_reminder.sent: start_reminder = self.start_reminder if start_reminder_time > timezone.now(): start_reminder.title_en = "Event" start_reminder.title_nl = "Evenement" start_reminder.body_en = f"'{self.title_en}' starts in " "1 hour" start_reminder.body_nl = f"'{self.title_nl}' begint over " "1 uur" start_reminder.category = Category.objects.get( key=Category.EVENT) start_reminder.time = start_reminder_time start_reminder.save() self.start_reminder = start_reminder if self.registration_required: self.start_reminder.users.set( [r.member for r in self.participants if r.member]) else: self.start_reminder.users.set(Member.current_members.all()) elif start_reminder.pk is not None: delete_collector.add([self.start_reminder]) self.start_reminder = None else: if (self.registration_reminder is not None and not self.registration_reminder.sent): delete_collector.add([self.registration_reminder]) self.registration_reminder = None if self.start_reminder is not None and not self.start_reminder.sent: delete_collector.add([self.start_reminder]) self.start_reminder = None super().save() delete_collector.delete() def delete(self, using=None, keep_parents=False): using = using or router.db_for_write(self.__class__, instance=self) collector = Collector(using=using) collector.collect([self], keep_parents=keep_parents) if (self.registration_reminder is not None and not self.registration_reminder.sent): collector.add([self.registration_reminder]) if self.start_reminder is not None and not self.start_reminder.sent: collector.add([self.start_reminder]) if self.is_pizza_event(): collector.add([self.pizzaevent]) return collector.delete() def __str__(self): return "{}: {}".format( self.title, timezone.localtime(self.start).strftime("%Y-%m-%d %H:%M")) class Meta: ordering = ("-start", ) permissions = (("override_organiser", "Can access events as if organizing"), )
class Slide(models.Model, metaclass=ModelTranslateMeta): """Describes an announcement""" title = CharField( verbose_name=_("Title"), help_text=_("The title of the slide; just for the admin."), blank=False, max_length=100, ) content = MultilingualField( ImageField, verbose_name=_("Content"), help_text=_("The content of the slide; what image to display."), blank=False, upload_to="public/announcements/slides/", ) since = models.DateTimeField( verbose_name=_("Display since"), help_text=_( "Hide this slide before this time. When all date- and " "time-fields are left blank, the slide won't " "be visible. It will, however, be visible on an event-page " "if it's linked to an event."), default=timezone.now, blank=True, null=True, ) until = models.DateTimeField( verbose_name=_("Display until"), help_text=_("Hide this slide after this time."), blank=True, null=True, ) order = models.PositiveIntegerField( verbose_name=_("Order"), help_text=_("Approximately where this slide " "should appear in the order"), default=0, ) members_only = models.BooleanField( verbose_name=_("Display only for authenticated members"), default=False) url = models.URLField( verbose_name=_("Link"), help_text=_("Place the user is taken to when clicking the slide"), blank=True, null=True, ) url_blank = models.BooleanField( verbose_name=_("Link outside thalia.nu"), help_text=_("Clicking the slide will open a new tab"), default=False, ) class Meta: ordering = ("-since", ) @property def is_visible(self): """Is this slide currently visible""" return ((self.until is None or self.until > timezone.now()) and (self.since is None or self.since <= timezone.now()) and not (self.since is None and self.until is None)) def __str__(self): return self.title