2012-07-12 04:38:39 +00:00
|
|
|
import os
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
from django.db import models
|
|
|
|
from django.db.models import Q
|
2020-11-22 12:21:54 +00:00
|
|
|
from django.urls import reverse
|
2012-07-18 23:21:21 +00:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2014-01-11 06:50:04 +00:00
|
|
|
from django.utils.timezone import now
|
2012-07-12 04:38:39 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
from django.contrib.auth import get_user_model
|
2015-06-16 23:30:41 +00:00
|
|
|
from django.core.exceptions import ObjectDoesNotExist
|
2015-07-26 05:21:02 +00:00
|
|
|
from django.core.exceptions import ValidationError
|
2012-07-12 04:38:39 +00:00
|
|
|
|
|
|
|
from model_utils.managers import InheritanceManager
|
2016-02-27 22:38:27 +00:00
|
|
|
from reversion import revisions as reversion
|
2012-07-12 04:38:39 +00:00
|
|
|
|
2017-05-07 03:22:28 +00:00
|
|
|
from symposion import constants
|
|
|
|
from symposion.text_parser import parse
|
2012-07-12 04:38:39 +00:00
|
|
|
from symposion.conference.models import Section
|
2014-12-18 14:17:35 +00:00
|
|
|
from symposion.speakers.models import Speaker
|
2012-07-12 04:38:39 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
User = get_user_model()
|
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
|
|
|
|
class ProposalSection(models.Model):
|
|
|
|
"""
|
|
|
|
configuration of proposal submissions for a specific Section.
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
a section is available for proposals iff:
|
|
|
|
* it is after start (if there is one) and
|
|
|
|
* it is before end (if there is one) and
|
|
|
|
* closed is NULL or False
|
|
|
|
"""
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
section = models.OneToOneField(
|
|
|
|
Section,
|
|
|
|
verbose_name=_("Section"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-06-26 03:46:09 +00:00
|
|
|
start = models.DateTimeField(null=True, blank=True, verbose_name=_("Start"))
|
|
|
|
end = models.DateTimeField(null=True, blank=True, verbose_name=_("End"))
|
|
|
|
closed = models.NullBooleanField(verbose_name=_("Closed"))
|
|
|
|
published = models.NullBooleanField(verbose_name=_("Published"))
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
@classmethod
|
|
|
|
def available(cls):
|
|
|
|
return cls._default_manager.filter(
|
2014-01-11 06:50:04 +00:00
|
|
|
Q(start__lt=now()) | Q(start=None),
|
|
|
|
Q(end__gt=now()) | Q(end=None),
|
2012-07-12 04:38:39 +00:00
|
|
|
Q(closed=False) | Q(closed=None),
|
|
|
|
)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-08-14 21:09:51 +00:00
|
|
|
def is_available(self):
|
|
|
|
if self.closed:
|
|
|
|
return False
|
2014-01-11 06:50:04 +00:00
|
|
|
if self.start and self.start > now():
|
2012-08-14 21:09:51 +00:00
|
|
|
return False
|
2014-01-11 06:50:04 +00:00
|
|
|
if self.end and self.end < now():
|
2012-08-14 21:09:51 +00:00
|
|
|
return False
|
|
|
|
return True
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-07-18 07:09:17 +00:00
|
|
|
def __str__(self):
|
2012-07-12 04:38:39 +00:00
|
|
|
return self.section.name
|
|
|
|
|
|
|
|
|
|
|
|
class ProposalKind(models.Model):
|
|
|
|
"""
|
|
|
|
e.g. talk vs panel vs tutorial vs poster
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
Note that if you have different deadlines, reviewers, etc. you'll want
|
|
|
|
to distinguish the section as well as the kind.
|
|
|
|
"""
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
section = models.ForeignKey(
|
|
|
|
Section,
|
|
|
|
related_name="proposal_kinds",
|
|
|
|
verbose_name=_("Section"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-18 23:21:21 +00:00
|
|
|
name = models.CharField(_("Name"), max_length=100)
|
2015-06-26 03:46:09 +00:00
|
|
|
slug = models.SlugField(verbose_name=_("Slug"))
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-07-18 07:09:17 +00:00
|
|
|
def __str__(self):
|
2012-07-12 04:38:39 +00:00
|
|
|
return self.name
|
|
|
|
|
|
|
|
|
|
|
|
class ProposalBase(models.Model):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
objects = InheritanceManager()
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
kind = models.ForeignKey(
|
|
|
|
ProposalKind,
|
|
|
|
verbose_name=_("Kind"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-06-26 03:46:09 +00:00
|
|
|
title = models.CharField(max_length=100, verbose_name=_("Title"))
|
2015-10-16 17:36:58 +00:00
|
|
|
abstract = models.TextField(
|
2016-06-19 02:39:28 +00:00
|
|
|
_("Abstract"),
|
2016-06-19 03:30:10 +00:00
|
|
|
help_text=_("This will appear in the conference programme. Up to about "
|
2017-05-07 03:22:28 +00:00
|
|
|
"500 words. " + constants.TEXT_FIELD_MONOSPACE_NOTE)
|
2012-07-12 04:38:39 +00:00
|
|
|
)
|
2015-10-16 17:36:58 +00:00
|
|
|
abstract_html = models.TextField(blank=True)
|
2016-06-19 02:39:28 +00:00
|
|
|
|
|
|
|
private_abstract = models.TextField(
|
|
|
|
_("Private Abstract"),
|
|
|
|
help_text=_("This will only be shown to organisers and reviewers. You "
|
|
|
|
"should provide any details about your proposal that you "
|
2017-05-07 03:22:28 +00:00
|
|
|
"don't want to be public here. " +
|
|
|
|
constants.TEXT_FIELD_MONOSPACE_NOTE)
|
2016-06-19 02:39:28 +00:00
|
|
|
)
|
|
|
|
private_abstract_html = models.TextField(blank=True)
|
|
|
|
|
|
|
|
technical_requirements = models.TextField(
|
|
|
|
_("Special Requirements"),
|
2012-07-12 04:38:39 +00:00
|
|
|
blank=True,
|
2016-06-19 02:39:28 +00:00
|
|
|
help_text=_("Speakers will be provided with: Internet access, power, "
|
|
|
|
"projector, audio. If you require anything in addition, "
|
|
|
|
"please list your technical requirements here. Such as: a "
|
|
|
|
"static IP address, A/V equipment or will be demonstrating "
|
2017-05-07 03:22:28 +00:00
|
|
|
"security-related techniques on the conference network. " +
|
|
|
|
constants.TEXT_FIELD_MONOSPACE_NOTE)
|
2012-07-12 04:38:39 +00:00
|
|
|
)
|
2016-06-19 02:39:28 +00:00
|
|
|
technical_requirements_html = models.TextField(blank=True)
|
|
|
|
|
2016-06-19 03:30:10 +00:00
|
|
|
project = models.CharField(
|
2016-06-19 02:39:28 +00:00
|
|
|
max_length=100,
|
|
|
|
blank=True,
|
|
|
|
help_text=_("The name of the project you will be talking about."),
|
|
|
|
)
|
|
|
|
project_url = models.URLField(
|
|
|
|
_("Project URL"),
|
|
|
|
blank=True,
|
|
|
|
help_text=_("If your project has a webpage, specify the URL here so "
|
|
|
|
"the committee can find out more about your proposal.")
|
|
|
|
)
|
|
|
|
video_url = models.URLField(
|
|
|
|
_("Video"),
|
|
|
|
blank=True,
|
|
|
|
help_text=_("You may optionally provide us with a link to a video of "
|
|
|
|
"you speaking at another event, or of a short 'elevator "
|
|
|
|
"pitch' of your proposed talk.")
|
|
|
|
)
|
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
submitted = models.DateTimeField(
|
2014-01-11 06:50:04 +00:00
|
|
|
default=now,
|
2012-07-12 04:38:39 +00:00
|
|
|
editable=False,
|
2015-06-26 03:46:09 +00:00
|
|
|
verbose_name=_("Submitted")
|
2012-07-12 04:38:39 +00:00
|
|
|
)
|
2020-11-22 12:21:54 +00:00
|
|
|
speaker = models.ForeignKey(
|
|
|
|
Speaker,
|
|
|
|
related_name="proposals",
|
|
|
|
verbose_name=_("Speaker"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
2015-07-26 05:21:02 +00:00
|
|
|
|
2015-10-16 17:36:58 +00:00
|
|
|
# @@@ this validation used to exist as a validators keyword on additional_speakers
|
|
|
|
# M2M field but that is no longer supported by Django. Should be moved to
|
|
|
|
# the form level
|
2015-07-26 05:21:02 +00:00
|
|
|
def additional_speaker_validator(self, a_speaker):
|
|
|
|
if a_speaker.speaker.email == self.speaker.email:
|
|
|
|
raise ValidationError(_("%s is same as primary speaker.") % a_speaker.speaker.email)
|
|
|
|
if a_speaker in [self.additional_speakers]:
|
|
|
|
raise ValidationError(_("%s has already been in speakers.") % a_speaker.speaker.email)
|
|
|
|
|
2014-12-18 14:17:35 +00:00
|
|
|
additional_speakers = models.ManyToManyField(Speaker, through="AdditionalSpeaker",
|
2015-10-16 17:36:58 +00:00
|
|
|
blank=True, verbose_name=_("Addtional speakers"))
|
2015-06-26 03:46:09 +00:00
|
|
|
cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled"))
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-10-16 17:36:58 +00:00
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
self.abstract_html = parse(self.abstract)
|
2016-06-19 02:39:28 +00:00
|
|
|
self.private_abstract_html = parse(self.private_abstract)
|
|
|
|
self.technical_requirements_html = parse(self.technical_requirements)
|
2015-10-16 17:36:58 +00:00
|
|
|
return super(ProposalBase, self).save(*args, **kwargs)
|
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
def can_edit(self):
|
|
|
|
return True
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-08-31 02:55:20 +00:00
|
|
|
@property
|
|
|
|
def section(self):
|
|
|
|
return self.kind.section
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
@property
|
|
|
|
def speaker_email(self):
|
|
|
|
return self.speaker.email
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
@property
|
|
|
|
def number(self):
|
|
|
|
return str(self.pk).zfill(3)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-06-16 23:30:41 +00:00
|
|
|
@property
|
|
|
|
def status(self):
|
|
|
|
try:
|
|
|
|
return self.result.status
|
|
|
|
except ObjectDoesNotExist:
|
2015-06-17 01:43:44 +00:00
|
|
|
return _('Undecided')
|
2015-06-16 23:30:41 +00:00
|
|
|
|
2017-09-22 06:37:36 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
def speakers(self):
|
|
|
|
yield self.speaker
|
2014-07-30 18:19:26 +00:00
|
|
|
speakers = self.additional_speakers.exclude(
|
|
|
|
additionalspeaker__status=AdditionalSpeaker.SPEAKING_STATUS_DECLINED)
|
|
|
|
for speaker in speakers:
|
2012-07-12 04:38:39 +00:00
|
|
|
yield speaker
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-09-09 01:38:57 +00:00
|
|
|
def notification_email_context(self):
|
|
|
|
return {
|
|
|
|
"title": self.title,
|
2016-09-03 05:06:01 +00:00
|
|
|
"main_speaker": self.speaker,
|
2015-06-11 16:36:11 +00:00
|
|
|
"speakers": ', '.join([x.name for x in self.speakers()]),
|
2016-09-03 02:48:31 +00:00
|
|
|
"additional_speakers": self.additional_speakers,
|
2012-09-09 01:38:57 +00:00
|
|
|
"kind": self.kind.name,
|
|
|
|
}
|
2012-07-12 04:38:39 +00:00
|
|
|
|
2015-07-18 07:09:17 +00:00
|
|
|
def __str__(self):
|
|
|
|
return self.title
|
|
|
|
|
2017-04-17 12:51:48 +00:00
|
|
|
|
2012-07-18 23:19:59 +00:00
|
|
|
reversion.register(ProposalBase)
|
|
|
|
|
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
class AdditionalSpeaker(models.Model):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
SPEAKING_STATUS_PENDING = 1
|
|
|
|
SPEAKING_STATUS_ACCEPTED = 2
|
|
|
|
SPEAKING_STATUS_DECLINED = 3
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
SPEAKING_STATUS = [
|
2012-07-18 23:21:21 +00:00
|
|
|
(SPEAKING_STATUS_PENDING, _("Pending")),
|
|
|
|
(SPEAKING_STATUS_ACCEPTED, _("Accepted")),
|
|
|
|
(SPEAKING_STATUS_DECLINED, _("Declined")),
|
2012-07-12 04:38:39 +00:00
|
|
|
]
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
speaker = models.ForeignKey(
|
|
|
|
Speaker,
|
|
|
|
verbose_name=_("Speaker"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
|
|
|
proposalbase = models.ForeignKey(
|
|
|
|
ProposalBase,
|
|
|
|
verbose_name=_("Proposalbase"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
2015-06-26 03:46:09 +00:00
|
|
|
status = models.IntegerField(choices=SPEAKING_STATUS, default=SPEAKING_STATUS_PENDING, verbose_name=_("Status"))
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
class Meta:
|
|
|
|
unique_together = ("speaker", "proposalbase")
|
2015-06-26 03:46:09 +00:00
|
|
|
verbose_name = _("Addtional speaker")
|
|
|
|
verbose_name_plural = _("Additional speakers")
|
2012-07-12 04:38:39 +00:00
|
|
|
|
2015-07-18 07:09:17 +00:00
|
|
|
def __str__(self):
|
2015-07-26 05:21:02 +00:00
|
|
|
if self.status is self.SPEAKING_STATUS_PENDING:
|
|
|
|
return _(u"pending speaker (%s)") % self.speaker.email
|
|
|
|
elif self.status is self.SPEAKING_STATUS_DECLINED:
|
|
|
|
return _(u"declined speaker (%s)") % self.speaker.email
|
|
|
|
else:
|
|
|
|
return self.speaker.name
|
|
|
|
|
2012-07-12 04:38:39 +00:00
|
|
|
|
|
|
|
def uuid_filename(instance, filename):
|
|
|
|
ext = filename.split(".")[-1]
|
|
|
|
filename = "%s.%s" % (uuid.uuid4(), ext)
|
|
|
|
return os.path.join("document", filename)
|
|
|
|
|
|
|
|
|
|
|
|
class SupportingDocument(models.Model):
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
proposal = models.ForeignKey(
|
|
|
|
ProposalBase,
|
|
|
|
related_name="supporting_documents",
|
|
|
|
verbose_name=_("Proposal"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2020-11-22 12:21:54 +00:00
|
|
|
uploaded_by = models.ForeignKey(
|
|
|
|
User,
|
|
|
|
verbose_name=_("Uploaded by"),
|
|
|
|
on_delete=models.CASCADE,
|
|
|
|
)
|
2014-12-15 21:15:46 +00:00
|
|
|
|
2015-06-26 03:46:09 +00:00
|
|
|
created_at = models.DateTimeField(default=now, verbose_name=_("Created at"))
|
2014-07-30 18:19:26 +00:00
|
|
|
|
2015-06-26 03:46:09 +00:00
|
|
|
file = models.FileField(upload_to=uuid_filename, verbose_name=_("File"))
|
|
|
|
description = models.CharField(max_length=140, verbose_name=_("Description"))
|
2012-07-12 04:38:39 +00:00
|
|
|
|
|
|
|
def download_url(self):
|
2017-05-07 06:17:29 +00:00
|
|
|
return self.file.url
|