bornhack-website/src/program/models.py

809 lines
22 KiB
Python
Raw Normal View History

import uuid
import os
import icalendar
import logging
from datetime import timedelta
from django.contrib.postgres.fields import DateTimeRangeField, ArrayField
from django.contrib import messages
2016-07-13 17:13:47 +00:00
from django.db import models
from django.core.exceptions import ObjectDoesNotExist, ValidationError
2016-08-07 13:49:30 +00:00
from django.utils.text import slugify
from django.conf import settings
2018-04-03 16:44:10 +00:00
from django.urls import reverse_lazy
from django.core.files.storage import FileSystemStorage
from django.urls import reverse
from django.apps import apps
from django.core.files.base import ContentFile
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from utils.models import CreatedUpdatedModel, CampRelatedModel
logger = logging.getLogger("bornhack.%s" % __name__)
class UrlType(CreatedUpdatedModel):
"""
Each Url object has a type.
"""
name = models.CharField(
max_length=25,
help_text='The name of this type',
unique=True,
)
icon = models.CharField(
max_length=100,
default='link',
help_text="Name of the fontawesome icon to use without the 'fa-' part"
)
class Meta:
ordering = ['name']
def __str__(self):
return self.name
class Url(CampRelatedModel):
"""
This model contains URLs related to
- SpeakerProposals
- EventProposals
- Speakers
- Events
Each URL has a UrlType and a GenericForeignKey to the model to which it belongs.
When a SpeakerProposal or EventProposal is approved the related URLs will be copied with FK to the new Speaker/Event objects.
"""
uuid = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
)
url = models.URLField(
help_text='The actual URL'
)
urltype = models.ForeignKey(
'program.UrlType',
help_text='The type of this URL',
on_delete=models.PROTECT,
)
speakerproposal = models.ForeignKey(
'program.SpeakerProposal',
null=True,
blank=True,
help_text='The speaker proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
eventproposal = models.ForeignKey(
'program.EventProposal',
null=True,
blank=True,
help_text='The event proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
speaker = models.ForeignKey(
'program.Speaker',
null=True,
blank=True,
help_text='The speaker proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
event = models.ForeignKey(
'program.Event',
null=True,
blank=True,
help_text='The event proposal object this URL belongs to',
on_delete=models.PROTECT,
related_name='urls',
)
def __str__(self):
return self.url
def clean(self):
''' Make sure we have exactly one FK '''
fks = 0
if self.speakerproposal:
fks += 1
if self.eventproposal:
fks += 1
if self.speaker:
fks += 1
if self.event:
fks += 1
if fks > 1:
raise(ValidationError("Url objects must have maximum one FK, this has %s" % fks))
@property
def owner(self):
"""
Return the object this Url belongs to
"""
if self.speakerproposal:
return self.speakerproposal
elif self.eventproposal:
return self.eventproposal
elif self.speaker:
return self.speaker
elif self.event:
return self.event
else:
return None
@property
def camp(self):
return self.owner.camp
###############################################################################
2017-03-07 23:07:12 +00:00
class UserSubmittedModel(CampRelatedModel):
2017-03-12 14:43:41 +00:00
"""
An abstract model containing the stuff that is shared
between the SpeakerProposal and EventProposal models.
2017-03-12 14:43:41 +00:00
"""
class Meta:
abstract = True
2017-03-12 14:43:41 +00:00
uuid = models.UUIDField(
primary_key=True,
default=uuid.uuid4,
editable=False,
)
user = models.ForeignKey(
'auth.User',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
PROPOSAL_PENDING = 'pending'
PROPOSAL_APPROVED = 'approved'
PROPOSAL_REJECTED = 'rejected'
PROPOSAL_STATUSES = [
PROPOSAL_PENDING,
PROPOSAL_APPROVED,
PROPOSAL_REJECTED,
]
PROPOSAL_STATUS_CHOICES = [
(PROPOSAL_PENDING, 'Pending approval'),
(PROPOSAL_APPROVED, 'Approved'),
(PROPOSAL_REJECTED, 'Rejected'),
]
proposal_status = models.CharField(
max_length=50,
choices=PROPOSAL_STATUS_CHOICES,
default=PROPOSAL_PENDING,
)
2016-07-13 17:13:47 +00:00
2017-03-12 14:43:41 +00:00
def __str__(self):
return '%s (submitted by: %s, status: %s)' % (self.headline, self.user, self.proposal_status)
2017-03-12 14:43:41 +00:00
def save(self, **kwargs):
if not self.camp.call_for_participation_open:
message = 'Call for participation is not open'
if hasattr(self, 'request'):
messages.error(self.request, message)
raise ValidationError(message)
super().save(**kwargs)
def delete(self, **kwargs):
if not self.camp.call_for_participation_open:
message = 'Call for participation is not open'
if hasattr(self, 'request'):
messages.error(self.request, message)
raise ValidationError(message)
super().delete(**kwargs)
2017-03-12 14:43:41 +00:00
class SpeakerProposal(UserSubmittedModel):
""" A speaker proposal """
2017-03-12 14:43:41 +00:00
camp = models.ForeignKey(
'camps.Camp',
2018-04-03 16:44:10 +00:00
related_name='speakerproposals',
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
name = models.CharField(
max_length=150,
help_text='Name or alias of the speaker/artist/host',
2017-03-12 14:43:41 +00:00
)
biography = models.TextField(
help_text='Biography of the speaker/artist/host. Markdown is supported.'
2017-03-12 14:43:41 +00:00
)
2017-07-15 13:56:32 +00:00
submission_notes = models.TextField(
help_text='Private notes for this speaker/artist/host. Only visible to the submitting user and the BornHack organisers.',
2017-07-15 13:56:32 +00:00
blank=True
)
needs_oneday_ticket = models.BooleanField(
default=False,
help_text='Check if BornHack needs to provide a free one-day ticket for this speaker',
)
2017-03-12 14:43:41 +00:00
@property
def headline(self):
return self.name
def get_absolute_url(self):
return reverse_lazy('program:speakerproposal_detail', kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid})
2017-03-12 14:43:41 +00:00
def mark_as_approved(self):
speakermodel = apps.get_model('program', 'speaker')
speakerproposalmodel = apps.get_model('program', 'speakerproposal')
speaker = speakermodel()
speaker.camp = self.camp
speaker.name = self.name
speaker.biography = self.biography
speaker.needs_oneday_ticket = self.needs_oneday_ticket
speaker.proposal = self
speaker.save()
self.proposal_status = speakerproposalmodel.PROPOSAL_APPROVED
self.save()
2017-03-12 14:43:41 +00:00
class EventProposal(UserSubmittedModel):
""" An event proposal """
2017-03-12 14:43:41 +00:00
track = models.ForeignKey(
'program.EventTrack',
2018-04-03 16:44:10 +00:00
related_name='eventproposals',
help_text='The track this event belongs to',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
title = models.CharField(
max_length=255,
help_text='The title of this event. Keep it short and memorable.',
2017-03-12 14:43:41 +00:00
)
abstract = models.TextField(
help_text='The abstract for this event. Describe what the audience can expect to see/hear.',
blank=True,
2017-03-12 14:43:41 +00:00
)
event_type = models.ForeignKey(
'program.EventType',
help_text='The type of event',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
speakers = models.ManyToManyField(
'program.SpeakerProposal',
2017-03-12 14:43:41 +00:00
blank=True,
2017-08-01 13:28:04 +00:00
help_text='Pick the speaker(s) for this event. If you cannot see anything here you need to go back and create Speaker Proposal(s) first.',
related_name='eventproposals',
2017-03-12 14:43:41 +00:00
)
allow_video_recording = models.BooleanField(
default=False,
help_text='Check to allow video recording of the event. Leave unchecked to avoid video recording.'
)
duration = models.IntegerField(
default=None,
null=True,
blank=True,
help_text='How much time (in minutes) should we set aside for this act? Please keep it between 60 and 180 minutes (1-3 hours).'
)
2017-07-15 13:56:32 +00:00
submission_notes = models.TextField(
2017-07-31 16:28:13 +00:00
help_text='Private notes for this event. Only visible to the submitting user and the BornHack organisers.',
2017-07-15 13:56:32 +00:00
blank=True
)
@property
def camp(self):
return self.track.camp
@property
2017-03-12 14:43:41 +00:00
def headline(self):
return self.title
def get_absolute_url(self):
return reverse_lazy(
'program:eventproposal_detail',
kwargs={'camp_slug': self.camp.slug, 'pk': self.uuid}
)
2017-03-12 14:43:41 +00:00
def get_available_speakerproposals(self):
"""
Return all SpeakerProposals submitted by the user who submitted this EventProposal,
which are not already added to this EventProposal
"""
return SpeakerProposal.objects.filter(
camp=self.track.camp,
user=self.user
).exclude(uuid__in=self.speakers.all().values_list('uuid'))
def mark_as_approved(self):
eventmodel = apps.get_model('program', 'event')
eventproposalmodel = apps.get_model('program', 'eventproposal')
event = eventmodel()
event.camp = self.camp
event.title = self.title
event.abstract = self.abstract
event.event_type = self.event_type
event.proposal = self
event.video_recording = self.allow_video_recording
event.save()
# loop through the speakerproposals linked to this eventproposal and associate any related speaker objects with this event
for sp in self.speakers.all():
try:
event.speakers.add(sp.speaker)
except ObjectDoesNotExist:
event.delete()
raise ValidationError('Not all speakers are approved or created yet.')
self.proposal_status = eventproposalmodel.PROPOSAL_APPROVED
self.save()
###############################################################################
2016-07-13 17:13:47 +00:00
class EventTrack(CampRelatedModel):
""" All events belong to a track. Administration of a track can be delegated to one or more users. """
name = models.CharField(
max_length=100
)
slug = models.SlugField()
camp = models.ForeignKey(
'camps.Camp',
related_name='eventtracks',
on_delete=models.PROTECT
)
managers = models.ManyToManyField(
'auth.User',
related_name='managed_tracks',
)
def __str__(self):
return self.name
class Meta:
unique_together = (('camp', 'slug'), ('camp', 'name'))
2018-05-20 18:08:25 +00:00
def serialize(self):
return {
"name": self.name,
"slug": self.slug,
}
2017-03-07 23:24:14 +00:00
class EventLocation(CampRelatedModel):
""" The places where stuff happens """
2017-03-12 14:43:41 +00:00
name = models.CharField(
max_length=100
)
slug = models.SlugField()
2017-03-12 14:43:41 +00:00
icon = models.CharField(
2017-03-14 22:25:37 +00:00
max_length=100,
help_text="Name of the fontawesome icon to use without the 'fa-' part"
2017-03-12 14:43:41 +00:00
)
camp = models.ForeignKey(
'camps.Camp',
2018-04-03 16:44:10 +00:00
related_name='eventlocations',
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
def __str__(self):
2017-07-12 09:36:23 +00:00
return '{} ({})'.format(self.name, self.camp)
class Meta:
unique_together = (('camp', 'slug'), ('camp', 'name'))
def serialize(self):
return {
"name": self.name,
"slug": self.slug,
"icon": self.icon,
}
2016-07-13 17:13:47 +00:00
class EventType(CreatedUpdatedModel):
2016-07-13 19:44:09 +00:00
""" Every event needs to have a type. """
2017-03-12 14:43:41 +00:00
name = models.CharField(
max_length=100,
2017-03-12 15:16:24 +00:00
unique=True,
help_text='The name of this event type',
2017-03-12 14:43:41 +00:00
)
2016-07-13 17:13:47 +00:00
slug = models.SlugField()
2017-03-12 14:43:41 +00:00
description = models.TextField(
default='',
help_text='The description of this type of event. Used in content submission flow.',
blank=True,
)
2017-03-12 14:43:41 +00:00
color = models.CharField(
2017-03-12 15:16:24 +00:00
max_length=50,
help_text='The background color of this event type',
2017-03-12 14:43:41 +00:00
)
light_text = models.BooleanField(
2017-03-12 15:16:24 +00:00
default=False,
help_text='Check if this event type should use white text color',
2017-03-12 14:43:41 +00:00
)
icon = models.CharField(
max_length=25,
help_text="Name of the fontawesome icon to use, without the 'fa-' part",
default='wrench',
)
2017-03-12 14:43:41 +00:00
notifications = models.BooleanField(
2017-03-12 15:16:24 +00:00
default=False,
help_text='Check to send notifications for this event type',
)
public = models.BooleanField(
default=False,
help_text='Check to permit users to submit events of this type',
2017-03-12 14:43:41 +00:00
)
2016-07-13 17:13:47 +00:00
2017-03-26 13:05:29 +00:00
include_in_event_list = models.BooleanField(
default=True,
help_text='Include events of this type in the event list?',
)
host_title = models.CharField(
max_length=30,
help_text='What to call someone hosting this type of event. Like "Artist" for Music or "Speaker" for talks.',
default='Person',
)
def __str__(self):
2016-07-13 20:37:20 +00:00
return self.name
def serialize(self):
return {
"name": self.name,
"slug": self.slug,
"color": self.color,
"light_text": self.light_text,
}
2016-07-13 17:13:47 +00:00
2017-03-12 14:43:41 +00:00
class Event(CampRelatedModel):
""" Something that is on the program one or more times. """
2017-03-12 14:43:41 +00:00
title = models.CharField(
max_length=255,
help_text='The title of this event',
)
abstract = models.TextField(
help_text='The abstract for this event'
)
event_type = models.ForeignKey(
'program.EventType',
help_text='The type of this event',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
slug = models.SlugField(
blank=True,
max_length=255,
help_text='The slug for this event, created automatically',
)
track = models.ForeignKey(
'program.EventTrack',
2017-03-12 14:43:41 +00:00
related_name='events',
help_text='The track this event belongs to',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
2016-08-08 17:36:13 +00:00
video_url = models.URLField(
max_length=1000,
null=True,
blank=True,
2017-03-12 14:43:41 +00:00
help_text='URL to the recording'
)
2017-03-12 14:43:41 +00:00
video_recording = models.BooleanField(
default=True,
2017-03-12 14:43:41 +00:00
help_text='Do we intend to record video of this event?'
)
proposal = models.OneToOneField(
'program.EventProposal',
null=True,
blank=True,
help_text='The event proposal object this event was created from',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
)
2016-08-08 17:36:13 +00:00
class Meta:
ordering = ['title']
unique_together = (('track', 'slug'), ('track', 'title'))
2016-07-13 19:44:09 +00:00
def __str__(self):
return '%s (%s)' % (self.title, self.camp.title)
2016-07-13 20:37:20 +00:00
2016-08-07 13:49:30 +00:00
def save(self, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
2016-08-07 13:49:30 +00:00
super(Event, self).save(**kwargs)
@property
def camp(self):
return self.track.camp
@property
def speakers_list(self):
if self.speakers.exists():
return ", ".join(self.speakers.all().values_list('name', flat=True))
return False
def get_absolute_url(self):
return reverse_lazy('program:event_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
def serialize(self):
data = {
'title': self.title,
'slug': self.slug,
'abstract': self.abstract,
2017-08-02 20:20:38 +00:00
'speaker_slugs': [
speaker.slug
for speaker in self.speakers.all()
],
'event_type': self.event_type.name,
}
if self.video_url:
video_state = 'has-recording'
data['video_url'] = self.video_url
elif self.video_recording:
video_state = 'to-be-recorded'
elif not self.video_recording:
video_state = 'not-to-be-recorded'
data['video_state'] = video_state
return data
2016-07-13 17:13:47 +00:00
2017-03-07 23:24:14 +00:00
class EventInstance(CampRelatedModel):
""" An instance of an event """
2017-03-12 14:43:41 +00:00
event = models.ForeignKey(
'program.event',
2018-04-03 16:44:10 +00:00
related_name='instances',
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
when = DateTimeRangeField()
2017-03-12 14:43:41 +00:00
notifications_sent = models.BooleanField(
default=False
)
location = models.ForeignKey(
'program.EventLocation',
2018-04-03 16:44:10 +00:00
related_name='eventinstances',
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
class Meta:
ordering = ['when']
def __str__(self):
return '%s (%s)' % (self.event, self.when)
def clean(self):
if self.location.camp != self.event.camp:
raise ValidationError({'location': 'Error: This location belongs to a different camp'})
@property
def camp(self):
return self.event.camp
@property
def schedule_date(self):
"""
Returns the schedule date of this eventinstance. Schedule date is determined by substracting
settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS from the eventinstance start time. This means that if
an event is scheduled for 00:30 wednesday evening (technically thursday) then the date
after substracting 5 hours would be wednesdays date, not thursdays
(given settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS=5)
"""
return (self.when.lower-timedelta(hours=settings.SCHEDULE_MIDNIGHT_OFFSET_HOURS)).date()
@property
def timeslots(self):
""" Find the number of timeslots this eventinstance takes up """
seconds = (self.when.upper-self.when.lower).seconds
minutes = seconds / 60
return minutes / settings.SCHEDULE_TIMESLOT_LENGTH_MINUTES
def get_ics_event(self):
ievent = icalendar.Event()
ievent['summary'] = self.event.title
ievent['description'] = self.event.abstract
ievent['dtstart'] = icalendar.vDatetime(self.when.lower).to_ical()
ievent['dtend'] = icalendar.vDatetime(self.when.upper).to_ical()
ievent['location'] = icalendar.vText(self.location.name)
return ievent
def serialize(self, user=None):
2017-04-16 00:10:24 +00:00
data = {
'title': self.event.title,
'slug': self.event.slug + '-' + str(self.id),
'event_slug': self.event.slug,
'from': self.when.lower.astimezone().isoformat(),
'to': self.when.upper.astimezone().isoformat(),
'url': str(self.event.get_absolute_url()),
'id': self.id,
2017-04-20 23:34:22 +00:00
'bg-color': self.event.event_type.color,
'fg-color': '#fff' if self.event.event_type.light_text else '#000',
'event_type': self.event.event_type.slug,
2018-05-20 18:08:25 +00:00
'event_track': self.event.track.slug,
2017-04-20 23:34:22 +00:00
'location': self.location.slug,
2017-04-29 10:23:01 +00:00
'location_icon': self.location.icon,
2017-04-26 22:23:03 +00:00
'timeslots': self.timeslots,
}
if self.event.video_url:
video_state = 'has-recording'
data['video_url'] = self.event.video_url
elif self.event.video_recording:
video_state = 'to-be-recorded'
elif not self.event.video_recording:
video_state = 'not-to-be-recorded'
data['video_state'] = video_state
if user and user.is_authenticated:
2017-04-16 00:10:24 +00:00
is_favorited = user.favorites.filter(event_instance=self).exists()
data['is_favorited'] = is_favorited
return data
2017-03-12 14:43:41 +00:00
class Speaker(CampRelatedModel):
""" A Person (co)anchoring one or more events on a camp. """
name = models.CharField(
max_length=150,
help_text='Name or alias of the speaker',
)
2017-03-11 11:12:07 +00:00
biography = models.TextField(
help_text='Markdown is supported.'
)
2017-03-12 14:43:41 +00:00
slug = models.SlugField(
blank=True,
max_length=255,
help_text='The slug for this speaker, will be autocreated',
)
camp = models.ForeignKey(
'camps.Camp',
null=True,
related_name='speakers',
help_text='The camp this speaker belongs to',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
2017-03-12 14:43:41 +00:00
)
2016-07-13 17:13:47 +00:00
events = models.ManyToManyField(
Event,
blank=True,
2017-03-12 14:43:41 +00:00
help_text='The event(s) this speaker is anchoring',
related_name='speakers'
2016-07-13 19:44:09 +00:00
)
2016-07-13 20:37:20 +00:00
proposal = models.OneToOneField(
'program.SpeakerProposal',
null=True,
2017-03-12 14:43:41 +00:00
blank=True,
help_text='The speaker proposal object this speaker was created from',
2018-04-03 16:44:10 +00:00
on_delete=models.PROTECT
)
needs_oneday_ticket = models.BooleanField(
default=False,
help_text='Check if BornHack needs to provide a free one-day ticket for this speaker',
)
2016-08-08 17:36:13 +00:00
class Meta:
ordering = ['name']
2017-03-12 14:43:41 +00:00
unique_together = (('camp', 'name'), ('camp', 'slug'))
2016-08-08 17:36:13 +00:00
def __str__(self):
2017-01-23 22:58:41 +00:00
return '%s (%s)' % (self.name, self.camp)
2016-08-08 17:36:13 +00:00
def save(self, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
2016-08-08 18:15:04 +00:00
super(Speaker, self).save(**kwargs)
2016-08-08 17:36:13 +00:00
def get_absolute_url(self):
return reverse_lazy('program:speaker_detail', kwargs={'camp_slug': self.camp.slug, 'slug': self.slug})
2017-08-02 20:20:38 +00:00
def serialize(self):
data = {
'name': self.name,
2017-08-02 20:20:38 +00:00
'slug': self.slug,
'biography': self.biography,
}
return data
2017-02-18 11:44:12 +00:00
2017-04-16 00:10:24 +00:00
class Favorite(models.Model):
2018-03-04 15:26:35 +00:00
user = models.ForeignKey(
'auth.User',
related_name='favorites',
on_delete=models.PROTECT
)
2018-04-03 16:44:10 +00:00
event_instance = models.ForeignKey(
'program.EventInstance',
on_delete=models.PROTECT
)
2017-04-16 00:10:24 +00:00
class Meta:
unique_together = ['user', 'event_instance']
# classes and functions below here was used by picture handling for speakers before it was removed in May 2018 by tyk
class CustomUrlStorage(FileSystemStorage):
"""
Must exist because it is mentioned in old migrations.
Can be removed when we clean up old migrations at some point
"""
pass
def get_speaker_picture_upload_path():
"""
Must exist because it is mentioned in old migrations.
Can be removed when we clean up old migrations at some point
"""
pass
def get_speakerproposal_picture_upload_path():
"""
Must exist because it is mentioned in old migrations.
Can be removed when we clean up old migrations at some point
"""
pass
def get_speakersubmission_picture_upload_path():
"""
Must exist because it is mentioned in old migrations.
Can be removed when we clean up old migrations at some point
"""
pass