2017-07-11 20:02:19 +00:00
import logging
2020-02-12 12:10:41 +00:00
import uuid
2017-04-13 09:48:50 +00:00
from datetime import timedelta
2018-06-03 15:16:35 +00:00
2020-02-12 12:10:41 +00:00
import icalendar
from django . apps import apps
from django . conf import settings
2017-06-05 12:52:59 +00:00
from django . contrib import messages
2020-02-12 12:10:41 +00:00
from django . contrib . postgres . fields import DateTimeRangeField
2017-07-15 16:15:14 +00:00
from django . core . exceptions import ObjectDoesNotExist , ValidationError
2017-03-14 20:45:12 +00:00
from django . core . files . storage import FileSystemStorage
2020-02-12 12:10:41 +00:00
from django . db import models
2020-02-22 13:50:09 +00:00
from django . urls import reverse , reverse_lazy
2020-02-12 12:10:41 +00:00
from django . utils . text import slugify
2020-02-22 13:50:09 +00:00
from utils . models import CampRelatedModel , CreatedUpdatedModel , UUIDModel
2018-05-23 21:28:27 +00:00
2017-07-11 20:02:19 +00:00
logger = logging . getLogger ( " bornhack. %s " % __name__ )
2017-04-13 09:48:50 +00:00
2017-03-14 20:45:12 +00:00
2018-05-23 21:28:27 +00:00
class UrlType ( CreatedUpdatedModel ) :
"""
Each Url object has a type .
"""
2019-06-16 12:32:24 +00:00
2018-05-23 21:28:27 +00:00
name = models . CharField (
2019-06-16 12:32:24 +00:00
max_length = 25 , help_text = " The name of this type " , unique = True
2018-05-23 21:28:27 +00:00
)
icon = models . CharField (
max_length = 100 ,
2019-06-16 12:32:24 +00:00
default = " fas fa-link " ,
help_text = " Name of the fontawesome icon to use, including the ' fab fa- ' or ' fas fa- ' part. " ,
2018-05-23 21:28:27 +00:00
)
class Meta :
2019-06-16 12:32:24 +00:00
ordering = [ " name " ]
2018-05-23 21:28:27 +00:00
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 .
"""
2019-06-16 12:32:24 +00:00
uuid = models . UUIDField ( primary_key = True , default = uuid . uuid4 , editable = False )
url = models . URLField ( help_text = " The actual URL " )
2018-05-23 21:28:27 +00:00
urltype = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.UrlType " , help_text = " The type of this URL " , on_delete = models . PROTECT
2018-05-23 21:28:27 +00:00
)
speakerproposal = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.SpeakerProposal " ,
2018-05-23 21:28:27 +00:00
null = True ,
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The speaker proposal object this URL belongs to " ,
2018-05-23 21:28:27 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " urls " ,
2018-05-23 21:28:27 +00:00
)
eventproposal = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.EventProposal " ,
2018-05-23 21:28:27 +00:00
null = True ,
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The event proposal object this URL belongs to " ,
2018-05-23 21:28:27 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " urls " ,
2018-05-23 21:28:27 +00:00
)
speaker = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.Speaker " ,
2018-05-23 21:28:27 +00:00
null = True ,
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The speaker proposal object this URL belongs to " ,
2018-05-23 21:28:27 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " urls " ,
2018-05-23 21:28:27 +00:00
)
event = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.Event " ,
2018-05-23 21:28:27 +00:00
null = True ,
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The event proposal object this URL belongs to " ,
2018-05-23 21:28:27 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " urls " ,
2018-05-23 21:28:27 +00:00
)
def __str__ ( self ) :
return self . url
def clean ( self ) :
2019-06-16 12:32:24 +00:00
""" Make sure we have exactly one FK """
2018-05-23 21:28:27 +00:00
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 :
2019-06-16 12:32:24 +00:00
raise (
ValidationError (
" Url objects must have maximum one FK, this has %s " % fks
)
)
2018-05-23 21:28:27 +00:00
@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
2018-07-17 18:46:30 +00:00
camp_filter = [
2019-06-16 12:32:24 +00:00
" speakerproposal__camp " ,
" eventproposal__track__camp " ,
" speaker__camp " ,
" event__track__camp " ,
2018-07-17 18:46:30 +00:00
]
2018-06-20 20:03:29 +00:00
2018-05-23 21:28:27 +00:00
###############################################################################
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
2017-03-12 18:06:03 +00:00
between the SpeakerProposal and EventProposal models .
2017-03-12 14:43:41 +00:00
"""
2017-03-07 23:00:17 +00:00
class Meta :
abstract = True
2019-06-16 12:32:24 +00:00
uuid = models . UUIDField ( primary_key = True , default = uuid . uuid4 , editable = False )
2017-03-12 14:43:41 +00:00
2019-06-16 12:32:24 +00:00
user = models . ForeignKey ( " auth.User " , on_delete = models . PROTECT )
2017-03-12 14:43:41 +00:00
2019-06-16 12:32:24 +00:00
PROPOSAL_PENDING = " pending "
PROPOSAL_APPROVED = " approved "
PROPOSAL_REJECTED = " rejected "
2017-03-07 23:00:17 +00:00
2020-02-12 12:10:41 +00:00
ROPOSAL_STATUSES = [ PROPOSAL_PENDING , PROPOSAL_APPROVED , PROPOSAL_REJECTED ]
2017-03-07 23:00:17 +00:00
2017-03-12 18:06:03 +00:00
PROPOSAL_STATUS_CHOICES = [
2019-06-16 12:32:24 +00:00
( PROPOSAL_PENDING , " Pending approval " ) ,
( PROPOSAL_APPROVED , " Approved " ) ,
( PROPOSAL_REJECTED , " Rejected " ) ,
2017-03-07 23:00:17 +00:00
]
2017-03-12 18:06:03 +00:00
proposal_status = models . CharField (
2019-06-16 12:32:24 +00:00
max_length = 50 , choices = PROPOSAL_STATUS_CHOICES , default = PROPOSAL_PENDING
2017-03-07 23:00:17 +00:00
)
2016-07-13 17:13:47 +00:00
2017-03-12 14:43:41 +00:00
def __str__ ( self ) :
2019-06-16 12:32:24 +00:00
return " %s (submitted by: %s , status: %s ) " % (
self . headline ,
self . user ,
self . proposal_status ,
)
2017-03-12 14:43:41 +00:00
2017-03-18 15:19:40 +00:00
def save ( self , * * kwargs ) :
2018-05-20 16:16:20 +00:00
if not self . camp . call_for_participation_open :
2019-06-16 12:32:24 +00:00
message = " Call for participation is not open "
if hasattr ( self , " request " ) :
2017-03-18 15:19:40 +00:00
messages . error ( self . request , message )
raise ValidationError ( message )
super ( ) . save ( * * kwargs )
def delete ( self , * * kwargs ) :
2018-05-20 16:16:20 +00:00
if not self . camp . call_for_participation_open :
2019-06-16 12:32:24 +00:00
message = " Call for participation is not open "
if hasattr ( self , " request " ) :
2017-03-18 15:19:40 +00:00
messages . error ( self . request , message )
raise ValidationError ( message )
super ( ) . delete ( * * kwargs )
2017-03-12 14:43:41 +00:00
2017-03-12 18:06:03 +00:00
class SpeakerProposal ( UserSubmittedModel ) :
""" A speaker proposal """
2017-03-12 14:43:41 +00:00
camp = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" camps.Camp " ,
related_name = " speakerproposals " ,
2018-06-03 13:34:04 +00:00
on_delete = models . PROTECT ,
editable = False ,
2017-03-12 14:43:41 +00:00
)
name = models . CharField (
2019-06-16 12:32:24 +00:00
max_length = 150 , help_text = " Name or alias of the speaker/artist/host "
2017-03-12 14:43:41 +00:00
)
2018-08-18 12:11:30 +00:00
email = models . EmailField (
max_length = 150 ,
2019-06-16 12:32:24 +00:00
help_text = " The email of the speaker (defaults to the logged in user if empty). " ,
2018-08-18 12:11:30 +00:00
)
2017-03-12 14:43:41 +00:00
biography = models . TextField (
2019-06-16 12:32:24 +00:00
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 (
2019-06-16 12:32:24 +00:00
help_text = " Private notes for this speaker/artist/host. Only visible to the submitting user and the BornHack organisers. " ,
blank = True ,
2017-07-15 13:56:32 +00:00
)
2018-05-20 16:16:20 +00:00
needs_oneday_ticket = models . BooleanField (
default = False ,
2019-06-16 12:32:24 +00:00
help_text = " Check if BornHack needs to provide a free one-day ticket for this speaker " ,
2018-05-20 16:16:20 +00:00
)
2017-03-12 14:43:41 +00:00
@property
def headline ( self ) :
return self . name
def get_absolute_url ( self ) :
2019-06-16 12:32:24 +00:00
return reverse_lazy (
" program:speakerproposal_detail " ,
kwargs = { " camp_slug " : self . camp . slug , " pk " : self . uuid } ,
)
2017-03-12 14:43:41 +00:00
2018-05-26 08:24:52 +00:00
def mark_as_approved ( self , request ) :
2018-06-03 13:34:04 +00:00
""" Marks a SpeakerProposal as approved, including creating/updating the related Speaker object """
2019-06-16 12:32:24 +00:00
speakerproposalmodel = apps . get_model ( " program " , " speakerproposal " )
2018-06-03 13:34:04 +00:00
# create a Speaker if we don't have one
2019-06-16 12:32:24 +00:00
if not hasattr ( self , " speaker " ) :
speakermodel = apps . get_model ( " program " , " speaker " )
2018-06-03 13:34:04 +00:00
speaker = speakermodel ( )
speaker . proposal = self
else :
speaker = self . speaker
# set Speaker data
2017-03-15 23:30:59 +00:00
speaker . camp = self . camp
2018-08-18 12:11:30 +00:00
if self . email :
email = self . email
else :
email = request . user . email
speaker . email = email
2017-03-15 23:30:59 +00:00
speaker . name = self . name
speaker . biography = self . biography
2018-05-20 16:16:20 +00:00
speaker . needs_oneday_ticket = self . needs_oneday_ticket
2017-03-15 23:30:59 +00:00
speaker . save ( )
2018-06-03 13:34:04 +00:00
# mark as approved and save
2017-03-15 23:30:59 +00:00
self . proposal_status = speakerproposalmodel . PROPOSAL_APPROVED
self . save ( )
2018-06-03 13:34:04 +00:00
# copy all the URLs to the speaker object
speaker . urls . clear ( )
2018-05-26 08:24:52 +00:00
for url in self . urls . all ( ) :
2019-06-16 12:32:24 +00:00
Url . objects . create ( url = url . url , urltype = url . urltype , speaker = speaker )
2018-05-26 08:24:52 +00:00
2018-06-03 13:34:04 +00:00
# a message to the admin
2019-06-16 12:32:24 +00:00
messages . success (
request , " Speaker object %s has been created/updated " % speaker
)
2018-06-03 13:34:04 +00:00
def mark_as_rejected ( self , request ) :
2019-06-16 12:32:24 +00:00
speakerproposalmodel = apps . get_model ( " program " , " speakerproposal " )
2018-06-03 13:34:04 +00:00
self . proposal_status = speakerproposalmodel . PROPOSAL_REJECTED
self . save ( )
messages . success ( request , " SpeakerProposal %s has been rejected " % self . name )
2018-05-26 08:24:52 +00:00
2017-03-12 14:43:41 +00:00
2017-03-12 18:06:03 +00:00
class EventProposal ( UserSubmittedModel ) :
""" An event proposal """
2019-06-16 12:32:24 +00:00
2018-05-20 16:16:20 +00:00
track = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.EventTrack " ,
related_name = " eventproposals " ,
help_text = " The track this event belongs to " ,
on_delete = models . PROTECT ,
2017-03-12 14:43:41 +00:00
)
title = models . CharField (
max_length = 255 ,
2019-06-16 12:32:24 +00:00
help_text = " The title of this event. Keep it short and memorable. " ,
2017-03-12 14:43:41 +00:00
)
abstract = models . TextField (
2019-06-16 12:32:24 +00:00
help_text = " The abstract for this event. Describe what the audience can expect to see/hear. " ,
2018-05-20 16:16:20 +00:00
blank = True ,
2017-03-12 14:43:41 +00:00
)
event_type = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.EventType " , help_text = " The type of event " , on_delete = models . PROTECT
2017-03-12 14:43:41 +00:00
)
speakers = models . ManyToManyField (
2019-06-16 12:32:24 +00:00
" program.SpeakerProposal " ,
2017-03-12 14:43:41 +00:00
blank = True ,
2019-06-16 12:32:24 +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
)
2017-07-09 19:24:25 +00:00
allow_video_recording = models . BooleanField (
2020-02-22 14:47:58 +00:00
default = False ,
help_text = " Uncheck to avoid video recording. Recordings are made available under the CC BY-SA 4.0 license. Uncheck if you can not accept this license. " ,
2018-05-20 16:16:20 +00:00
)
duration = models . IntegerField (
default = None ,
null = True ,
blank = True ,
2019-06-16 12:32:24 +00:00
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-09 19:24:25 +00:00
)
2017-07-15 13:56:32 +00:00
submission_notes = models . TextField (
2019-06-16 12:32:24 +00:00
help_text = " Private notes for this event. Only visible to the submitting user and the BornHack organisers. " ,
blank = True ,
2017-07-15 13:56:32 +00:00
)
2019-07-31 10:47:38 +00:00
use_provided_speaker_laptop = models . BooleanField (
2019-08-01 07:34:00 +00:00
help_text = " Will you be using the provided speaker laptop? " , default = True
2019-07-31 10:47:38 +00:00
)
2018-05-20 16:16:20 +00:00
@property
def camp ( self ) :
return self . track . camp
2019-06-16 12:32:24 +00:00
camp_filter = " track__camp "
2018-06-20 20:03:29 +00:00
2017-03-09 23:45:50 +00:00
@property
2017-03-12 14:43:41 +00:00
def headline ( self ) :
return self . title
def get_absolute_url ( self ) :
2017-06-05 12:52:59 +00:00
return reverse_lazy (
2019-06-16 12:32:24 +00:00
" program:eventproposal_detail " ,
kwargs = { " camp_slug " : self . camp . slug , " pk " : self . uuid } ,
2017-06-05 12:52:59 +00:00
)
2017-03-12 14:43:41 +00:00
2018-05-20 16:16:20 +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 (
2019-06-16 12:32:24 +00:00
camp = self . track . camp , user = self . user
) . exclude ( uuid__in = self . speakers . all ( ) . values_list ( " uuid " ) )
2018-05-20 16:16:20 +00:00
2018-05-26 08:24:52 +00:00
def mark_as_approved ( self , request ) :
2019-06-16 12:32:24 +00:00
eventmodel = apps . get_model ( " program " , " event " )
eventproposalmodel = apps . get_model ( " program " , " eventproposal " )
2018-07-17 20:43:16 +00:00
# use existing event if we have one
2019-06-16 12:32:24 +00:00
if not hasattr ( self , " event " ) :
2018-07-17 20:43:16 +00:00
event = eventmodel ( )
2018-07-24 17:02:58 +00:00
else :
event = self . event
2018-05-26 08:24:52 +00:00
event . track = self . track
2017-03-15 23:30:59 +00:00
event . title = self . title
event . abstract = self . abstract
event . event_type = self . event_type
event . proposal = self
2017-07-11 03:24:08 +00:00
event . video_recording = self . allow_video_recording
2017-03-15 23:30:59 +00:00
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 ( ) :
2017-07-15 14:48:30 +00:00
try :
2017-04-17 18:29:59 +00:00
event . speakers . add ( sp . speaker )
2017-07-15 14:48:30 +00:00
except ObjectDoesNotExist :
2018-07-24 17:02:58 +00:00
# clean up
event . urls . clear ( )
2017-07-15 15:17:19 +00:00
event . delete ( )
2019-06-16 12:32:24 +00:00
raise ValidationError ( " Not all speakers are approved or created yet. " )
2017-03-15 23:30:59 +00:00
self . proposal_status = eventproposalmodel . PROPOSAL_APPROVED
self . save ( )
2018-07-17 20:43:16 +00:00
# clear any old urls from the event object and copy all the URLs from the proposal
event . urls . clear ( )
2018-05-26 08:24:52 +00:00
for url in self . urls . all ( ) :
2019-06-16 12:32:24 +00:00
Url . objects . create ( url = url . url , urltype = url . urltype , event = event )
2018-05-26 08:24:52 +00:00
2018-07-17 20:43:16 +00:00
messages . success ( request , " Event object %s has been created/updated " % event )
2018-05-20 16:16:20 +00:00
2018-06-03 21:36:07 +00:00
def mark_as_rejected ( self , request ) :
2019-06-16 12:32:24 +00:00
eventproposalmodel = apps . get_model ( " program " , " eventproposal " )
2018-06-03 21:36:07 +00:00
self . proposal_status = eventproposalmodel . PROPOSAL_REJECTED
self . save ( )
messages . success ( request , " EventProposal %s has been rejected " % self . title )
2017-06-05 12:52:59 +00:00
###############################################################################
2017-03-09 23:45:50 +00:00
2016-07-13 17:13:47 +00:00
2018-05-20 16:16:20 +00:00
class EventTrack ( CampRelatedModel ) :
""" All events belong to a track. Administration of a track can be delegated to one or more users. """
2019-06-16 12:32:24 +00:00
name = models . CharField ( max_length = 100 , help_text = " The name of this Track " )
2018-05-20 16:16:20 +00:00
2019-06-16 12:32:24 +00:00
slug = models . SlugField ( help_text = " The url slug for this Track " )
2018-05-20 16:16:20 +00:00
camp = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" camps.Camp " ,
related_name = " eventtracks " ,
2018-06-03 13:34:04 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
help_text = " The Camp this Track belongs to " ,
2018-05-20 16:16:20 +00:00
)
managers = models . ManyToManyField (
2019-06-16 12:32:24 +00:00
" auth.User " ,
related_name = " managed_tracks " ,
2018-06-03 13:34:04 +00:00
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " If this track is managed by someone other than the Content team pick the users here. " ,
2018-05-20 16:16:20 +00:00
)
def __str__ ( self ) :
return self . name
class Meta :
2019-06-16 12:32:24 +00:00
unique_together = ( ( " camp " , " slug " ) , ( " camp " , " name " ) )
2018-05-20 16:16:20 +00:00
2018-05-20 18:08:25 +00:00
def serialize ( self ) :
2019-06-16 12:32:24 +00:00
return { " name " : self . name , " slug " : self . slug }
2018-05-20 18:08:25 +00:00
2018-05-20 16:16:20 +00:00
2017-03-07 23:24:14 +00:00
class EventLocation ( CampRelatedModel ) :
2017-02-08 22:34:24 +00:00
""" The places where stuff happens """
2017-03-12 14:43:41 +00:00
2019-06-16 12:32:24 +00:00
name = models . CharField ( max_length = 100 )
2017-03-12 14:43:41 +00:00
2017-02-08 22:34:24 +00:00
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 ,
2019-06-16 12:32:24 +00:00
help_text = " Name of the fontawesome icon to use without the ' fa- ' part " ,
2017-03-12 14:43:41 +00:00
)
camp = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" camps.Camp " , related_name = " eventlocations " , on_delete = models . PROTECT
2017-03-12 14:43:41 +00:00
)
2017-02-08 22:34:24 +00:00
def __str__ ( self ) :
2019-06-16 12:32:24 +00:00
return " {} ( {} ) " . format ( self . name , self . camp )
2017-02-08 22:34:24 +00:00
class Meta :
2019-06-16 12:32:24 +00:00
unique_together = ( ( " camp " , " slug " ) , ( " camp " , " name " ) )
2017-02-08 22:34:24 +00:00
2017-07-17 09:25:57 +00:00
def serialize ( self ) :
2019-06-16 12:32:24 +00:00
return { " name " : self . name , " slug " : self . slug , " icon " : self . icon }
2017-07-15 23:31:00 +00:00
2017-02-08 22:34:24 +00:00
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. """
2019-06-16 12:32:24 +00:00
2017-03-12 14:43:41 +00:00
name = models . CharField (
2019-06-16 12:32:24 +00:00
max_length = 100 , 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
2018-05-20 16:16:20 +00:00
description = models . TextField (
2019-06-16 12:32:24 +00:00
default = " " ,
help_text = " The description of this type of event. Used in content submission flow. " ,
2018-05-20 16:16:20 +00:00
blank = True ,
)
2017-03-12 14:43:41 +00:00
color = models . CharField (
2019-06-16 12:32: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 (
2019-06-16 12:32: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
)
2018-05-20 16:16:20 +00:00
icon = models . CharField (
max_length = 25 ,
help_text = " Name of the fontawesome icon to use, without the ' fa- ' part " ,
2019-06-16 12:32:24 +00:00
default = " wrench " ,
2018-05-20 16:16:20 +00:00
)
2017-03-12 14:43:41 +00:00
notifications = models . BooleanField (
2019-06-16 12:32:24 +00:00
default = False , help_text = " Check to send notifications for this event type "
2017-03-12 15:16:24 +00:00
)
public = models . BooleanField (
2019-06-16 12:32:24 +00:00
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 (
2019-06-16 12:32:24 +00:00
default = True , help_text = " Include events of this type in the event list? "
2017-03-26 13:05:29 +00:00
)
2018-05-20 16:16:20 +00:00
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. ' ,
2019-06-16 12:32:24 +00:00
default = " Person " ,
2018-05-20 16:16:20 +00:00
)
2017-01-31 22:39:49 +00:00
def __str__ ( self ) :
2016-07-13 20:37:20 +00:00
return self . name
2017-07-17 09:25:57 +00:00
def serialize ( self ) :
2017-07-15 23:31:00 +00:00
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 ) :
2017-01-20 15:18:10 +00:00
""" Something that is on the program one or more times. """
2017-03-12 14:43:41 +00:00
2019-06-16 12:32:24 +00:00
title = models . CharField ( max_length = 255 , help_text = " The title of this event " )
2017-03-12 14:43:41 +00:00
2019-06-16 12:32:24 +00:00
abstract = models . TextField ( help_text = " The abstract for this event " )
2017-03-12 14:43:41 +00:00
event_type = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.EventType " ,
help_text = " The type of this event " ,
on_delete = models . PROTECT ,
2017-03-12 14:43:41 +00:00
)
slug = models . SlugField (
blank = True ,
max_length = 255 ,
2019-06-16 12:32:24 +00:00
help_text = " The slug for this event, created automatically " ,
2017-03-12 14:43:41 +00:00
)
2018-05-20 16:16:20 +00:00
track = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.EventTrack " ,
related_name = " events " ,
help_text = " The track this event belongs to " ,
on_delete = models . PROTECT ,
2017-03-12 14:43:41 +00:00
)
2016-08-08 17:36:13 +00:00
2017-02-23 20:51:36 +00:00
video_url = models . URLField (
2019-06-16 12:32:24 +00:00
max_length = 1000 , null = True , blank = True , help_text = " URL to the recording "
2017-02-23 20:51:36 +00:00
)
2017-03-12 14:43:41 +00:00
2017-02-23 20:51:36 +00:00
video_recording = models . BooleanField (
2019-06-16 12:32:24 +00:00
default = True , help_text = " Do we intend to record video of this event? "
2017-02-23 20:51:36 +00:00
)
2017-08-22 09:48:51 +00:00
proposal = models . OneToOneField (
2019-06-16 12:32:24 +00:00
" program.EventProposal " ,
2017-08-22 09:48:51 +00:00
null = True ,
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The event proposal object this event was created from " ,
2018-06-03 13:34:04 +00:00
on_delete = models . PROTECT ,
editable = False ,
2017-08-22 09:48:51 +00:00
)
2016-08-08 17:36:13 +00:00
class Meta :
2019-06-16 12:32:24 +00:00
ordering = [ " title " ]
unique_together = ( ( " track " , " slug " ) , ( " track " , " title " ) )
2016-07-13 19:44:09 +00:00
2017-01-31 22:39:49 +00:00
def __str__ ( self ) :
2019-06-16 12:32:24 +00:00
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 :
2016-08-08 19:56:54 +00:00
self . slug = slugify ( self . title )
2016-08-07 13:49:30 +00:00
super ( Event , self ) . save ( * * kwargs )
2018-05-20 16:16:20 +00:00
@property
def camp ( self ) :
return self . track . camp
2019-06-16 12:32:24 +00:00
camp_filter = " track__camp "
2018-06-20 20:03:29 +00:00
2017-01-24 23:24:04 +00:00
@property
def speakers_list ( self ) :
2017-04-17 18:29:59 +00:00
if self . speakers . exists ( ) :
2019-06-16 12:32:24 +00:00
return " , " . join ( self . speakers . all ( ) . values_list ( " name " , flat = True ) )
2017-01-24 23:24:04 +00:00
return False
2017-03-07 23:00:17 +00:00
def get_absolute_url ( self ) :
2020-02-22 14:09:12 +00:00
return reverse (
2019-06-16 12:32:24 +00:00
" program:event_detail " ,
2020-02-22 13:50:09 +00:00
kwargs = { " camp_slug " : self . camp . slug , " event_slug " : self . slug } ,
2019-06-16 12:32:24 +00:00
)
2017-03-07 23:00:17 +00:00
2017-07-17 09:25:57 +00:00
def serialize ( self ) :
data = {
2019-06-16 12:32:24 +00:00
" title " : self . title ,
" slug " : self . slug ,
" abstract " : self . abstract ,
" speaker_slugs " : [ speaker . slug for speaker in self . speakers . all ( ) ] ,
" event_type " : self . event_type . name ,
2017-07-17 09:25:57 +00:00
}
2017-07-18 11:35:17 +00:00
if self . video_url :
2019-06-16 12:32:24 +00:00
video_state = " has-recording "
data [ " video_url " ] = self . video_url
2017-08-03 20:58:12 +00:00
elif self . video_recording :
2019-06-16 12:32:24 +00:00
video_state = " to-be-recorded "
2017-08-03 20:58:12 +00:00
elif not self . video_recording :
2019-06-16 12:32:24 +00:00
video_state = " not-to-be-recorded "
2017-08-03 20:58:12 +00:00
2019-06-16 12:32:24 +00:00
data [ " video_state " ] = video_state
2017-07-18 11:35:17 +00:00
2017-07-17 09:25:57 +00:00
return data
2016-07-13 17:13:47 +00:00
2017-03-07 23:24:14 +00:00
class EventInstance ( CampRelatedModel ) :
2016-12-25 14:52:55 +00:00
""" An instance of an event """
2017-03-12 14:43:41 +00:00
2020-02-22 14:09:12 +00:00
uuid = models . UUIDField (
default = uuid . uuid4 ,
unique = True ,
editable = False ,
help_text = " This field is mostly here to keep Frab happy, it is not the PK of the model " ,
)
2017-03-12 14:43:41 +00:00
event = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.event " , related_name = " instances " , on_delete = models . PROTECT
2017-03-12 14:43:41 +00:00
)
2017-01-20 15:18:10 +00:00
when = DateTimeRangeField ( )
2017-03-12 14:43:41 +00:00
2019-06-16 12:32:24 +00:00
notifications_sent = models . BooleanField ( default = False )
2017-03-12 14:43:41 +00:00
location = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.EventLocation " , related_name = " eventinstances " , on_delete = models . PROTECT
2017-03-12 14:43:41 +00:00
)
2016-12-25 14:52:55 +00:00
class Meta :
2019-06-16 12:32:24 +00:00
ordering = [ " when " ]
2016-12-25 14:52:55 +00:00
2017-01-31 22:39:49 +00:00
def __str__ ( self ) :
2019-06-16 12:32:24 +00:00
return " %s ( %s ) " % ( self . event , self . when )
2016-12-25 14:52:55 +00:00
2017-07-15 16:15:14 +00:00
def clean ( self ) :
2017-02-08 22:34:24 +00:00
if self . location . camp != self . event . camp :
2019-06-16 12:32:24 +00:00
raise ValidationError (
{ " location " : " Error: This location belongs to a different camp " }
)
2017-01-22 11:59:57 +00:00
2017-03-07 20:44:30 +00:00
@property
def camp ( self ) :
return self . event . camp
2019-06-16 12:32:24 +00:00
camp_filter = " event__track__camp "
2018-06-20 20:03:29 +00:00
2017-01-22 11:59:57 +00:00
@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
2017-02-08 22:34:24 +00:00
after substracting 5 hours would be wednesdays date , not thursdays
( given settings . SCHEDULE_MIDNIGHT_OFFSET_HOURS = 5 )
2017-01-22 11:59:57 +00:00
"""
2019-06-16 12:32:24 +00:00
return (
self . when . lower - timedelta ( hours = settings . SCHEDULE_MIDNIGHT_OFFSET_HOURS )
) . date ( )
2017-01-22 11:59:57 +00:00
@property
def timeslots ( self ) :
2018-05-20 16:16:20 +00:00
""" Find the number of timeslots this eventinstance takes up """
2019-06-16 12:32:24 +00:00
seconds = ( self . when . upper - self . when . lower ) . seconds
2017-01-22 11:59:57 +00:00
minutes = seconds / 60
return minutes / settings . SCHEDULE_TIMESLOT_LENGTH_MINUTES
2017-04-13 09:48:50 +00:00
def get_ics_event ( self ) :
ievent = icalendar . Event ( )
2019-06-16 12:32:24 +00:00
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 )
2017-04-13 09:48:50 +00:00
return ievent
2017-07-17 09:25:57 +00:00
def serialize ( self , user = None ) :
2017-04-16 00:10:24 +00:00
data = {
2019-06-16 12:32:24 +00:00
" title " : self . event . title ,
" slug " : self . event . slug + " - " + str ( self . id ) ,
" event_slug " : self . event . slug ,
" from " : self . when . lower . isoformat ( ) ,
" to " : self . when . upper . isoformat ( ) ,
" url " : str ( self . event . get_absolute_url ( ) ) ,
" id " : self . id ,
" 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 ,
" event_track " : self . event . track . slug ,
" location " : self . location . slug ,
" location_icon " : self . location . icon ,
" timeslots " : self . timeslots ,
2017-04-15 17:35:18 +00:00
}
2017-07-15 16:16:27 +00:00
if self . event . video_url :
2019-06-16 12:32:24 +00:00
video_state = " has-recording "
data [ " video_url " ] = self . event . video_url
2017-08-03 20:58:12 +00:00
elif self . event . video_recording :
2019-06-16 12:32:24 +00:00
video_state = " to-be-recorded "
2017-08-03 20:58:12 +00:00
elif not self . event . video_recording :
2019-06-16 12:32:24 +00:00
video_state = " not-to-be-recorded "
2017-08-03 20:58:12 +00:00
2019-06-16 12:32:24 +00:00
data [ " video_state " ] = video_state
2017-07-15 16:16:27 +00:00
2017-04-17 18:29:59 +00:00
if user and user . is_authenticated :
2017-04-16 00:10:24 +00:00
is_favorited = user . favorites . filter ( event_instance = self ) . exists ( )
2019-06-16 12:32:24 +00:00
data [ " is_favorited " ] = is_favorited
2017-04-16 00:10:24 +00:00
return data
2017-04-15 17:35:18 +00:00
2017-03-12 14:43:41 +00:00
class Speaker ( CampRelatedModel ) :
""" A Person (co)anchoring one or more events on a camp. """
2019-06-16 12:32:24 +00:00
name = models . CharField ( max_length = 150 , help_text = " Name or alias of the speaker " )
2017-03-12 14:43:41 +00:00
2019-06-16 12:32:24 +00:00
email = models . EmailField ( max_length = 150 , help_text = " The email of the speaker. " )
2018-08-18 12:11:30 +00:00
2019-06-16 12:32:24 +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 ,
2019-06-16 12:32:24 +00:00
help_text = " The slug for this speaker, will be autocreated " ,
2017-03-12 14:43:41 +00:00
)
camp = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" camps.Camp " ,
2017-03-12 14:43:41 +00:00
null = True ,
2019-06-16 12:32:24 +00:00
related_name = " speakers " ,
help_text = " The camp this speaker belongs to " ,
on_delete = models . PROTECT ,
2017-03-12 14:43:41 +00:00
)
2016-07-13 17:13:47 +00:00
events = models . ManyToManyField (
Event ,
2016-08-04 21:03:39 +00:00
blank = True ,
2019-06-16 12:32:24 +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
2017-03-12 18:06:03 +00:00
proposal = models . OneToOneField (
2019-06-16 12:32:24 +00:00
" program.SpeakerProposal " ,
2017-03-07 23:00:17 +00:00
null = True ,
2017-03-12 14:43:41 +00:00
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The speaker proposal object this speaker was created from " ,
2018-06-03 13:34:04 +00:00
on_delete = models . PROTECT ,
editable = False ,
2017-03-07 23:00:17 +00:00
)
2018-05-20 16:16:20 +00:00
needs_oneday_ticket = models . BooleanField (
default = False ,
2019-06-16 12:32:24 +00:00
help_text = " Check if BornHack needs to provide a free one-day ticket for this speaker " ,
2018-05-20 16:16:20 +00:00
)
2016-08-08 17:36:13 +00:00
class Meta :
2019-06-16 12:32:24 +00:00
ordering = [ " name " ]
unique_together = ( ( " camp " , " name " ) , ( " camp " , " slug " ) )
2016-08-08 17:36:13 +00:00
2017-02-18 11:30:26 +00:00
def __str__ ( self ) :
2019-06-16 12:32:24 +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 :
2016-08-08 19:56:54 +00:00
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
2017-03-07 23:00:17 +00:00
def get_absolute_url ( self ) :
2019-06-16 12:32:24 +00:00
return reverse_lazy (
" program:speaker_detail " ,
kwargs = { " camp_slug " : self . camp . slug , " slug " : self . slug } ,
)
2017-08-02 20:20:38 +00:00
2017-07-17 09:25:57 +00:00
def serialize ( self ) :
2019-06-16 12:32:24 +00:00
data = { " name " : self . name , " slug " : self . slug , " biography " : self . biography }
2017-07-17 09:25:57 +00:00
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 (
2019-06-16 12:32:24 +00:00
" auth.User " , related_name = " favorites " , on_delete = models . PROTECT
2018-03-04 15:26:35 +00:00
)
2018-04-03 16:44:10 +00:00
event_instance = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" program.EventInstance " , on_delete = models . PROTECT
2018-04-03 16:44:10 +00:00
)
2017-04-16 00:10:24 +00:00
class Meta :
2019-06-16 12:32:24 +00:00
unique_together = [ " user " , " event_instance " ]
2017-04-16 00:10:24 +00:00
2020-02-22 13:50:09 +00:00
###############################################################################
class EventFeedback ( CampRelatedModel , UUIDModel ) :
"""
This model contains all feedback for Events
Each user can submit exactly one feedback per Event
"""
class Meta :
unique_together = [ ( " user " , " event " ) ]
YESNO_CHOICES = [ ( True , " Yes " ) , ( False , " No " ) ]
user = models . ForeignKey (
" auth.User " ,
on_delete = models . PROTECT ,
help_text = " The User who wrote this feedback " ,
)
event = models . ForeignKey (
" program.event " ,
related_name = " feedbacks " ,
on_delete = models . PROTECT ,
help_text = " The Event this feedback is about " ,
)
expectations_fulfilled = models . BooleanField (
choices = YESNO_CHOICES , help_text = " Did the event live up to your expectations? " ,
)
attend_speaker_again = models . BooleanField (
choices = YESNO_CHOICES ,
help_text = " Would you attend another event with the same speaker? " ,
)
RATING_CHOICES = [ ( n , f " { n } " ) for n in range ( 0 , 6 ) ]
rating = models . IntegerField (
choices = RATING_CHOICES , help_text = " Rating/Score (5 is best) " ,
)
comment = models . TextField ( blank = True , help_text = " Any other comments or feedback? " )
approved = models . NullBooleanField (
help_text = " Approve feedback? It will not be visible to the Event owner before it is approved. "
)
@property
def camp ( self ) :
return self . event . camp
camp_filter = " event__track__camp "
def get_absolute_url ( self ) :
return reverse (
" program:eventfeedback_detail " ,
kwargs = { " camp_slug " : self . camp . slug , " event_slug " : self . event . slug } ,
)
2018-05-20 16:16:20 +00:00
# classes and functions below here was used by picture handling for speakers before it was removed in May 2018 by tyk
2019-06-16 12:32:24 +00:00
2018-05-20 16:16:20 +00:00
class CustomUrlStorage ( FileSystemStorage ) :
"""
Must exist because it is mentioned in old migrations .
Can be removed when we clean up old migrations at some point
"""
2019-06-16 12:32:24 +00:00
2018-05-20 16:16:20 +00:00
pass
2019-06-16 12:32:24 +00:00
2018-05-20 16:16:20 +00:00
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
2019-06-16 12:32:24 +00:00
2018-05-20 16:16:20 +00:00
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
2019-06-16 12:32:24 +00:00
2018-05-20 16:16:20 +00:00
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