bornhack-website/src/camps/models.py

232 lines
7.7 KiB
Python
Raw Normal View History

2015-10-03 01:07:05 +00:00
from django.db import models
from utils.models import UUIDModel, CreatedUpdatedModel
from program.models import EventType, EventLocation
from django.contrib.postgres.fields import DateTimeRangeField
from psycopg2.extras import DateTimeTZRange
from django.core.exceptions import ValidationError
from datetime import timedelta
from django.utils import timezone
2017-02-19 20:20:19 +00:00
from django.urls import reverse
2017-03-23 17:32:13 +00:00
import logging
2019-06-16 12:32:24 +00:00
2017-03-23 17:32:13 +00:00
logger = logging.getLogger("bornhack.%s" % __name__)
2015-10-03 01:07:05 +00:00
class Permission(models.Model):
"""
An unmanaged field-less model which holds our non-model permissions (such as team permission sets)
"""
2019-06-16 12:32:24 +00:00
class Meta:
managed = False
2019-06-16 12:32:24 +00:00
default_permissions = ()
permissions = (
("backoffice_permission", "BackOffice access"),
("orgateam_permission", "Orga Team permissions set"),
("infoteam_permission", "Info Team permissions set"),
("economyteam_permission", "Economy Team permissions set"),
("contentteam_permission", "Content Team permissions set"),
("expense_create_permission", "Expense Create permission"),
("revenue_create_permission", "Revenue Create permission"),
)
2015-10-03 01:07:05 +00:00
class Camp(CreatedUpdatedModel, UUIDModel):
class Meta:
2019-06-16 12:32:24 +00:00
verbose_name = "Camp"
verbose_name_plural = "Camps"
ordering = ["-title"]
2015-10-03 01:07:05 +00:00
title = models.CharField(
2019-06-16 12:32:24 +00:00
verbose_name="Title",
help_text="Title of the camp, ie. Bornhack 2016.",
2015-10-03 01:07:05 +00:00
max_length=255,
)
tagline = models.CharField(
2019-06-16 12:32:24 +00:00
verbose_name="Tagline",
help_text='Tagline of the camp, ie. "Initial Commit"',
max_length=255,
2015-10-03 01:07:05 +00:00
)
slug = models.SlugField(
2019-06-16 12:32:24 +00:00
verbose_name="Url Slug", help_text="The url slug to use for this camp"
2015-10-03 01:07:05 +00:00
)
Merge teamcomms branch. Refactor team app and add events app. * Primary commit towards improved team communications. Add new events app to handle team notifications when various events happen, with a Type model which contain event types and a Routing model which controls routing of events to teams. Add shortslug for Camp and Team models. events.handler.py contains the code for sending irc and email notifications for teams. The first two eventtypes have been added in datamigrations, 'ticket_created' and 'public_credit_name_changed', and the tickets and profile apps have been adjusted accordingly. Team IRC channels can be marked as managed and if so the IRC bot will register the team channel with ChanServ if possible. Team IRC channels can be marked as private and the bot will set invite only and maintain an ACL with team members. Users can set their NickServ username in their profile to get on the ACL. Rework all team views and templates. Remove TeamArea model and make Team have an FK to Camp directly. Add docstrings a whole bunch of places. Move signal handlers to apps.py and signal_handlers.py in a few apps. Add basic team mailing list handling, more work to be done. Update bootstrap-devsite script to add more teammembers and add some team event routing for the two eventtypes we have. * default to the console backend for email unless we specifically ask for realworld email * fix signal for public_credit_name approval irc message * fix name display on /people/ page * fix the text on people pages when all non-responsible team members are anonymous * handle cases where we fallback to the area responsible properly * readd removed property, it is used in team_detail view * make it possible to filter profiles by public_credit_name_approved * add method for sending IRC messages in ircbot.utils.add_irc_message(), extend periodic bot method to do more than check for outgoing messages so rename it, refactor chanserv and nickserv handling code, create methods to check and join/part IRC channels as needed, maintain channel ACLs for private channels, do not autojoin any channels when instatiating the bot instead rely on the new check_irc_channels() method to join them, rename profile presave signal, add checking for changed nickserv usernames for acl handling, add teammember.irc_channel_acl_ok boolean to track ACL state, add missing help_text properties to TeamMember fields, rename teammember postsave signal, add teammember deleted signal, readd wrongly deleted EnsureTeamMemberResponsibleMixin * add a few missing early returns
2018-04-09 21:11:05 +00:00
shortslug = models.SlugField(
2019-06-16 12:32:24 +00:00
verbose_name="Short Slug",
help_text="Abbreviated version of the slug. Used in IRC channel names and other places with restricted name length.",
Merge teamcomms branch. Refactor team app and add events app. * Primary commit towards improved team communications. Add new events app to handle team notifications when various events happen, with a Type model which contain event types and a Routing model which controls routing of events to teams. Add shortslug for Camp and Team models. events.handler.py contains the code for sending irc and email notifications for teams. The first two eventtypes have been added in datamigrations, 'ticket_created' and 'public_credit_name_changed', and the tickets and profile apps have been adjusted accordingly. Team IRC channels can be marked as managed and if so the IRC bot will register the team channel with ChanServ if possible. Team IRC channels can be marked as private and the bot will set invite only and maintain an ACL with team members. Users can set their NickServ username in their profile to get on the ACL. Rework all team views and templates. Remove TeamArea model and make Team have an FK to Camp directly. Add docstrings a whole bunch of places. Move signal handlers to apps.py and signal_handlers.py in a few apps. Add basic team mailing list handling, more work to be done. Update bootstrap-devsite script to add more teammembers and add some team event routing for the two eventtypes we have. * default to the console backend for email unless we specifically ask for realworld email * fix signal for public_credit_name approval irc message * fix name display on /people/ page * fix the text on people pages when all non-responsible team members are anonymous * handle cases where we fallback to the area responsible properly * readd removed property, it is used in team_detail view * make it possible to filter profiles by public_credit_name_approved * add method for sending IRC messages in ircbot.utils.add_irc_message(), extend periodic bot method to do more than check for outgoing messages so rename it, refactor chanserv and nickserv handling code, create methods to check and join/part IRC channels as needed, maintain channel ACLs for private channels, do not autojoin any channels when instatiating the bot instead rely on the new check_irc_channels() method to join them, rename profile presave signal, add checking for changed nickserv usernames for acl handling, add teammember.irc_channel_acl_ok boolean to track ACL state, add missing help_text properties to TeamMember fields, rename teammember postsave signal, add teammember deleted signal, readd wrongly deleted EnsureTeamMemberResponsibleMixin * add a few missing early returns
2018-04-09 21:11:05 +00:00
)
buildup = DateTimeRangeField(
2019-06-16 12:32:24 +00:00
verbose_name="Buildup Period", help_text="The camp buildup period."
2016-05-06 20:33:59 +00:00
)
2019-06-16 12:32:24 +00:00
camp = DateTimeRangeField(verbose_name="Camp Period", help_text="The camp period.")
teardown = DateTimeRangeField(
2019-06-16 12:32:24 +00:00
verbose_name="Teardown period", help_text="The camp teardown period."
)
read_only = models.BooleanField(
2019-06-16 12:32:24 +00:00
help_text="Whether the camp is read only (i.e. in the past)", default=False
)
2017-08-14 17:10:58 +00:00
colour = models.CharField(
2019-06-16 12:32:24 +00:00
verbose_name="Colour",
help_text="The primary colour for the camp in hex",
max_length=7,
2017-08-14 17:10:58 +00:00
)
light_text = models.BooleanField(
default=True,
2019-06-16 12:32:24 +00:00
help_text="Check if this camps colour requires white text, uncheck if black text is better",
)
call_for_participation_open = models.BooleanField(
2019-06-16 12:32:24 +00:00
help_text="Check if the Call for Participation is open for this camp",
default=False,
)
call_for_participation = models.TextField(
blank=True,
2019-06-16 12:32:24 +00:00
help_text="The CFP markdown for this Camp",
default="The Call For Participation for this Camp has not been written yet",
)
call_for_sponsors_open = models.BooleanField(
2019-06-16 12:32:24 +00:00
help_text="Check if the Call for Sponsors is open for this camp", default=False
)
call_for_sponsors = models.TextField(
blank=True,
2019-06-16 12:32:24 +00:00
help_text="The CFS markdown for this Camp",
default="The Call For Sponsors for this Camp has not been written yet",
)
show_schedule = models.BooleanField(
help_text="Check if the schedule should be shown.", default=True
)
2017-02-19 20:20:19 +00:00
def get_absolute_url(self):
2019-06-16 12:32:24 +00:00
return reverse("camp_detail", kwargs={"camp_slug": self.slug})
2017-02-19 20:20:19 +00:00
def clean(self):
2019-06-16 12:32:24 +00:00
""" Make sure the dates make sense - meaning no overlaps and buildup before camp before teardown """
errors = []
# check for overlaps buildup vs. camp
if self.buildup.upper > self.camp.lower:
msg = "End of buildup must not be after camp start"
2019-06-16 12:32:24 +00:00
errors.append(ValidationError({"buildup", msg}))
errors.append(ValidationError({"camp", msg}))
# check for overlaps camp vs. teardown
if self.camp.upper > self.teardown.lower:
msg = "End of camp must not be after teardown start"
2019-06-16 12:32:24 +00:00
errors.append(ValidationError({"camp", msg}))
errors.append(ValidationError({"teardown", msg}))
if errors:
raise ValidationError(errors)
def __str__(self):
return "%s - %s" % (self.title, self.tagline)
2016-07-13 19:44:09 +00:00
@property
def event_types(self):
""" Return all event types with at least one event in this camp """
2019-06-16 12:32:24 +00:00
return EventType.objects.filter(
event__instances__isnull=False, event__camp=self
).distinct()
2015-10-03 01:07:05 +00:00
@property
def event_locations(self):
2019-06-16 12:32:24 +00:00
""" Return all event locations with at least one event in this camp"""
return EventLocation.objects.filter(
eventinstances__isnull=False, camp=self
).distinct()
@property
def logo_small(self):
2019-06-16 12:32:24 +00:00
return "img/%(slug)s/logo/%(slug)s-logo-s.png" % {"slug": self.slug}
2015-10-03 01:07:05 +00:00
@property
def logo_small_svg(self):
2019-06-16 12:32:24 +00:00
return "img/%(slug)s/logo/%(slug)s-logo-small.svg" % {"slug": self.slug}
@property
def logo_large(self):
2019-06-16 12:32:24 +00:00
return "img/%(slug)s/logo/%(slug)s-logo-l.png" % {"slug": self.slug}
@property
def logo_large_svg(self):
2019-06-16 12:32:24 +00:00
return "img/%(slug)s/logo/%(slug)s-logo-large.svg" % {"slug": self.slug}
def get_days(self, camppart):
2019-06-16 12:32:24 +00:00
"""
Returns a list of DateTimeTZRanges representing the days during the specified part of the camp.
2019-06-16 12:32:24 +00:00
"""
if not hasattr(self, camppart):
2017-03-23 17:32:13 +00:00
logger.error("nonexistant field/attribute")
return False
field = getattr(self, camppart)
2019-06-16 12:32:24 +00:00
if (
not hasattr(field, "__class__")
or not hasattr(field.__class__, "__name__")
or not field.__class__.__name__ == "DateTimeTZRange"
):
2017-03-23 17:32:13 +00:00
logger.error("this attribute is not a datetimetzrange field: %s" % field)
return False
daycount = (field.upper - field.lower).days
days = []
for i in range(0, daycount):
if i == 0:
# on the first day use actual start time instead of midnight
days.append(
DateTimeTZRange(
field.lower,
2019-06-16 12:32:24 +00:00
(field.lower + timedelta(days=i + 1)).replace(hour=0),
)
)
2019-06-16 12:32:24 +00:00
elif i == daycount - 1:
# on the last day use actual end time instead of midnight
days.append(
DateTimeTZRange(
2019-06-16 12:32:24 +00:00
(field.lower + timedelta(days=i)).replace(hour=0),
field.lower + timedelta(days=i + 1),
)
)
else:
# neither first nor last day, goes from midnight to midnight
days.append(
DateTimeTZRange(
2019-06-16 12:32:24 +00:00
(field.lower + timedelta(days=i)).replace(hour=0),
(field.lower + timedelta(days=i + 1)).replace(hour=0),
)
)
return days
@property
def buildup_days(self):
2019-06-16 12:32:24 +00:00
"""
Returns a list of DateTimeTZRanges representing the days during the buildup.
2019-06-16 12:32:24 +00:00
"""
return self.get_days("buildup")
@property
def camp_days(self):
2019-06-16 12:32:24 +00:00
"""
Returns a list of DateTimeTZRanges representing the days during the camp.
2019-06-16 12:32:24 +00:00
"""
return self.get_days("camp")
@property
def teardown_days(self):
2019-06-16 12:32:24 +00:00
"""
Returns a list of DateTimeTZRanges representing the days during the buildup.
2019-06-16 12:32:24 +00:00
"""
return self.get_days("teardown")