PonyConf/cfp/models.py

348 lines
12 KiB
Python

from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core.urlresolvers import reverse
from django.core.validators import MaxValueValidator, MinValueValidator
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q, Sum, Avg, Case, When
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.utils.translation import ugettext, ugettext_lazy as _
from autoslug import AutoSlugField
from colorful.fields import RGBColorField
import uuid
from datetime import timedelta
from ponyconf.utils import PonyConfModel
from mailing.models import MessageThread
class Conference(models.Model):
site = models.OneToOneField(Site, on_delete=models.CASCADE)
name = models.CharField(blank=True, max_length=100, verbose_name=_('Conference name'))
home = models.TextField(blank=True, default="", verbose_name=_('Homepage (markdown)'))
venue = models.TextField(blank=True, default="", verbose_name=_('Venue information'))
city = models.CharField(max_length=64, blank=True, default="", verbose_name=_('City'))
contact_email = models.CharField(max_length=100, blank=True, verbose_name=_('Contact email'))
reply_email = models.CharField(max_length=100, blank=True, verbose_name=_('Reply email'))
staff = models.ManyToManyField(User, blank=True, verbose_name=_('Staff members'))
secure_domain = models.BooleanField(default=True, verbose_name=_('Secure domain (HTTPS)'))
custom_css = models.TextField(blank=True)
external_css_link = models.URLField(blank=True)
#subscriptions_open = models.BooleanField(default=False) # workshop subscription
#def cfp_is_open(self):
# events = Event.objects.filter(site=self.site)
# return any(map(lambda x: x.is_open(), events))
@property
def opened_categories(self):
now = timezone.now()
return TalkCategory.objects.filter(site=self.site)\
.filter(Q(opening_date__isnull=True) | Q(opening_date__lte=now))\
.filter(Q(closing_date__isnull=True) | Q(closing_date__gte=now))
def from_email(self):
return self.name+' <'+self.contact_email+'>'
def clean_fields(self, exclude=None):
super().clean_fields(exclude)
if self.reply_email is not None:
try:
self.reply_email.format(token='a' * 80)
except Exception:
raise ValidationError({
'reply_email': _('The reply email should be a formatable string accepting a token argument (e.g. ponyconf+{token}@exemple.com).'),
})
def __str__(self):
return str(self.site)
class ParticipantManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.annotate(
accepted_talk_count=Sum(Case(When(talk__accepted=True, then=1), default=0, output_field=models.IntegerField())),
pending_talk_count=Sum(Case(When(talk__accepted=None, then=1), default=0, output_field=models.IntegerField())),
refused_talk_count=Sum(Case(When(talk__accepted=False, then=1), default=0, output_field=models.IntegerField())),
)
return qs
class Participant(PonyConfModel):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
name = models.CharField(max_length=128, verbose_name=_('Your Name'))
email = models.EmailField()
biography = models.TextField(verbose_name=_('Biography'))
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
twitter = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Twitter'))
linkedin = models.CharField(max_length=100, blank=True, default='', verbose_name=_('LinkedIn'))
github = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Github'))
website = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Website'))
facebook = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Facebook'))
mastodon = models.CharField(max_length=100, blank=True, default='', verbose_name=_('Mastodon'))
phone_number = models.CharField(max_length=64, blank=True, default='', verbose_name=_('Phone number'))
language = models.CharField(max_length=10, blank=True)
notes = models.TextField(default='', blank=True, verbose_name=_("Notes"), help_text=_('This field is only visible by organizers.'))
vip = models.BooleanField(default=False)
conversation = models.OneToOneField(MessageThread)
objects = ParticipantManager()
def get_absolute_url(self):
return reverse('participant-details', kwargs={'participant_id': self.token})
class Meta:
# A User can participe only once to a Conference (= Site)
unique_together = ('site', 'email')
def __str__(self):
return str(self.name)
@property
def accepted_talk_set(self):
return self.talk_set.filter(accepted=True)
@property
def pending_talk_set(self):
return self.talk_set.filter(accepted=None)
@property
def refused_talk_set(self):
return self.talk_set.filter(accepted=False)
class Track(PonyConfModel):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
name = models.CharField(max_length=128, verbose_name=_('Name'))
slug = AutoSlugField(populate_from='name')
description = models.TextField(blank=True, verbose_name=_('Description'))
#managers = models.ManyToManyField(User, blank=True, verbose_name=_('Managers'))
class Meta:
unique_together = ('site', 'name')
def estimated_duration(self):
return sum([talk.estimated_duration for talk in self.talk_set.all()])
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('talk-list') + '?track=%s' % self.slug
class Room(models.Model):
site = models.ForeignKey(Site, on_delete=models.CASCADE)
slug = AutoSlugField(populate_from='name')
name = models.CharField(max_length=256, blank=True, default="")
label = models.CharField(max_length=256, blank=True, default="")
capacity = models.IntegerField(default=0)
class Meta:
unique_together = ['site', 'name']
ordering = ['name']
def __str__(self):
return self.name
def get_absolute_url(self):
return reverse('room-details', kwargs={'slug': self.slug})
@property
def talks(self):
return self.talk_set.exclude(accepted=False)
@property
def talks_by_date(self):
return self.talks.filter(start_date__isnull=False).exclude(duration=0, category__duration=0).order_by('start_date').all()
@property
def unscheduled_talks(self):
return self.talks.filter(Q(start_date__isnull=True) | Q(duration=0, category__duration=0)).all()
class TalkCategory(models.Model): # type of talk (conf 30min, 1h, stand, …)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
name = models.CharField(max_length=64)
duration = models.PositiveIntegerField(default=0, verbose_name=_('Default duration (min)'))
color = RGBColorField(default='#ffffff', verbose_name=_("Color on program"))
label = models.CharField(max_length=64, verbose_name=_("Label on program"), blank=True, default="")
opening_date = models.DateTimeField(null=True, blank=True, default=None)
closing_date = models.DateTimeField(null=True, blank=True, default=None)
def is_open(self):
now = timezone.now()
if self.opening_date and now < self.opening_date:
return False
if self.closing_date and now > self.closing_date:
return False
return True
class Meta:
unique_together = ('site', 'name')
ordering = ('pk',)
verbose_name = "category"
verbose_name_plural = "categories"
def __str__(self):
return ugettext(self.name)
def get_absolute_url(self):
return reverse('talk-list') + '?category=%d' % self.pk
#class Attendee(PonyConfModel):
#
# user = models.ForeignKey(User, null=True)
# name = models.CharField(max_length=64, blank=True, default="")
# email = models.EmailField(blank=True, default="")
#
# def get_name(self):
# if self.user:
# return str(self.user.profile)
# else:
# return self.name
# get_name.short_description = _('Name')
#
# def get_email(self):
# if self.user:
# return self.user.email
# else:
# return self.email
# get_email.short_description = _('Email')
#
# def __str__(self):
# return self.get_name()
#def talk_materials_destination(talk, filename):
# return join(talk.site.name, talk.slug, filename)
class TalkManager(models.Manager):
def get_queryset(self):
qs = super().get_queryset()
qs = qs.annotate(score=Coalesce(Avg('vote__vote'), 0))
return qs
class Talk(PonyConfModel):
LICENCES = (
('CC-Zero CC-BY', 'CC-Zero CC-BY'),
('CC-BY-SA', 'CC-BY-SA'),
('CC-BY-ND', 'CC-BY-ND'),
('CC-BY-NC', 'CC-BY-NC'),
('CC-BY-NC-SA','CC-BY-NC-SA'),
('CC-BY-NC-ND', 'CC-BY-NC-ND'),
)
site = models.ForeignKey(Site, on_delete=models.CASCADE)
speakers = models.ManyToManyField(Participant, verbose_name=_('Speakers'))
title = models.CharField(max_length=128, verbose_name=_('Talk Title'))
slug = AutoSlugField(populate_from='title', unique=True)
#abstract = models.CharField(max_length=255, blank=True, verbose_name=_('Abstract'))
description = models.TextField(verbose_name=_('Description of your talk'))
track = models.ForeignKey(Track, blank=True, null=True, verbose_name=_('Track'))
notes = models.TextField(blank=True, verbose_name=_('Message to organizers'), help_text=_('If you have any constraint or if you have anything that may help you to select your talk, like a video or slides of your talk, please write it down here'))
category = models.ForeignKey(TalkCategory, verbose_name=_('Talk Category'))
videotaped = models.BooleanField(_("I'm ok to be recorded on video"), default=True)
video_licence = models.CharField(choices=LICENCES, default='CC-BY-SA', max_length=10, verbose_name=_("Video licence"))
sound = models.BooleanField(_("I need sound"), default=False)
accepted = models.NullBooleanField(default=None)
start_date = models.DateTimeField(null=True, blank=True, default=None, verbose_name=_('Beginning date and time'))
duration = models.PositiveIntegerField(default=0, verbose_name=_('Duration (min)'))
room = models.ForeignKey(Room, blank=True, null=True, default=None)
plenary = models.BooleanField(default=False)
#materials = models.FileField(null=True, upload_to=talk_materials_destination, verbose_name=_('Materials'),
# help_text=_('You can use this field to share some materials related to your intervention.'))
token = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
conversation = models.OneToOneField(MessageThread)
objects = TalkManager()
class Meta:
ordering = ('title',)
def __str__(self):
return self.title
def get_speakers_str(self):
speakers = list(map(str, self.speakers.all()))
if len(speakers) == 0:
return 'superman'
elif len(speakers) == 1:
return speakers[0]
else:
return ', '.join(speakers[:-1]) + ' & ' + str(speakers[-1])
@property
def estimated_duration(self):
return self.duration or self.category.duration
def get_absolute_url(self):
return reverse('talk-details', kwargs={'talk_id': self.token})
@property
def end_date(self):
if self.estimated_duration:
return self.start_date + timedelta(minutes=self.estimated_duration)
else:
return None
@property
def dtstart(self):
return self.start_date.strftime('%Y%m%dT%H%M%SZ')
@property
def dtend(self):
return self.end_date.strftime('%Y%m%dT%H%M%SZ')
#@property
#def materials_name(self):
# return basename(self.materials.name)
class Meta:
ordering = ('category__id', 'title',)
class Vote(PonyConfModel):
talk = models.ForeignKey(Talk)
user = models.ForeignKey(User)
vote = models.IntegerField(validators=[MinValueValidator(-2), MaxValueValidator(2)], default=0)
class Meta:
unique_together = ('talk', 'user')
def __str__(self):
return "%+i by %s for %s" % (self.vote, self.user, self.talk)
def get_absolute_url(self):
return self.talk.get_absolute_url()