
Drop markdown for monospace With markdown, we're only allowing about 1/10 of what is possible. This creates a false reality and expectation. Lets not suggest this. Nor do we have in-editor preview. So the user has to save, look at it, then go back and edit. And seeing a bunch of sanitized HTML or just missing sections isn't firendly. Monospace, what you type, is what you're going to get. It gives the presenter enough power to build a readable abstract, but not so much that they can break the page and it's CSS, nor the ability to confuse onselve through not getting what you expect. We keep bleach sanitation and we should probably run linkify on this in the long term. (Turn links into clickable links)
252 lines
8.6 KiB
Python
252 lines
8.6 KiB
Python
import os
|
|
import uuid
|
|
|
|
from django.core.urlresolvers import reverse
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.utils.translation import ugettext_lazy as _
|
|
from django.utils.timezone import now
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.core.exceptions import ObjectDoesNotExist
|
|
from django.core.exceptions import ValidationError
|
|
|
|
from model_utils.managers import InheritanceManager
|
|
from reversion import revisions as reversion
|
|
|
|
from symposion import constants
|
|
from symposion.text_parser import parse
|
|
from symposion.conference.models import Section
|
|
from symposion.speakers.models import Speaker
|
|
|
|
|
|
class ProposalSection(models.Model):
|
|
"""
|
|
configuration of proposal submissions for a specific Section.
|
|
|
|
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
|
|
"""
|
|
|
|
section = models.OneToOneField(Section, verbose_name=_("Section"))
|
|
|
|
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"))
|
|
|
|
@classmethod
|
|
def available(cls):
|
|
return cls._default_manager.filter(
|
|
Q(start__lt=now()) | Q(start=None),
|
|
Q(end__gt=now()) | Q(end=None),
|
|
Q(closed=False) | Q(closed=None),
|
|
)
|
|
|
|
def is_available(self):
|
|
if self.closed:
|
|
return False
|
|
if self.start and self.start > now():
|
|
return False
|
|
if self.end and self.end < now():
|
|
return False
|
|
return True
|
|
|
|
def __str__(self):
|
|
return self.section.name
|
|
|
|
|
|
class ProposalKind(models.Model):
|
|
"""
|
|
e.g. talk vs panel vs tutorial vs poster
|
|
|
|
Note that if you have different deadlines, reviewers, etc. you'll want
|
|
to distinguish the section as well as the kind.
|
|
"""
|
|
|
|
section = models.ForeignKey(Section, related_name="proposal_kinds", verbose_name=_("Section"))
|
|
|
|
name = models.CharField(_("Name"), max_length=100)
|
|
slug = models.SlugField(verbose_name=_("Slug"))
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class ProposalBase(models.Model):
|
|
|
|
objects = InheritanceManager()
|
|
|
|
kind = models.ForeignKey(ProposalKind, verbose_name=_("Kind"))
|
|
|
|
title = models.CharField(max_length=100, verbose_name=_("Title"))
|
|
abstract = models.TextField(
|
|
_("Abstract"),
|
|
help_text=_("This will appear in the conference programme. Up to about "
|
|
"500 words. " + constants.TEXT_FIELD_MONOSPACE_NOTE)
|
|
)
|
|
abstract_html = models.TextField(blank=True)
|
|
|
|
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 "
|
|
"don't want to be public here. " +
|
|
constants.TEXT_FIELD_MONOSPACE_NOTE)
|
|
)
|
|
private_abstract_html = models.TextField(blank=True)
|
|
|
|
technical_requirements = models.TextField(
|
|
_("Special Requirements"),
|
|
blank=True,
|
|
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 "
|
|
"security-related techniques on the conference network. " +
|
|
constants.TEXT_FIELD_MONOSPACE_NOTE)
|
|
)
|
|
technical_requirements_html = models.TextField(blank=True)
|
|
|
|
project = models.CharField(
|
|
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.")
|
|
)
|
|
|
|
submitted = models.DateTimeField(
|
|
default=now,
|
|
editable=False,
|
|
verbose_name=_("Submitted")
|
|
)
|
|
speaker = models.ForeignKey(Speaker, related_name="proposals", verbose_name=_("Speaker"))
|
|
|
|
# @@@ 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
|
|
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)
|
|
|
|
additional_speakers = models.ManyToManyField(Speaker, through="AdditionalSpeaker",
|
|
blank=True, verbose_name=_("Addtional speakers"))
|
|
cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled"))
|
|
|
|
def save(self, *args, **kwargs):
|
|
self.abstract_html = parse(self.abstract)
|
|
self.private_abstract_html = parse(self.private_abstract)
|
|
self.technical_requirements_html = parse(self.technical_requirements)
|
|
return super(ProposalBase, self).save(*args, **kwargs)
|
|
|
|
def can_edit(self):
|
|
return True
|
|
|
|
@property
|
|
def section(self):
|
|
return self.kind.section
|
|
|
|
@property
|
|
def speaker_email(self):
|
|
return self.speaker.email
|
|
|
|
@property
|
|
def number(self):
|
|
return str(self.pk).zfill(3)
|
|
|
|
@property
|
|
def status(self):
|
|
try:
|
|
return self.result.status
|
|
except ObjectDoesNotExist:
|
|
return _('Undecided')
|
|
|
|
def speakers(self):
|
|
yield self.speaker
|
|
speakers = self.additional_speakers.exclude(
|
|
additionalspeaker__status=AdditionalSpeaker.SPEAKING_STATUS_DECLINED)
|
|
for speaker in speakers:
|
|
yield speaker
|
|
|
|
def notification_email_context(self):
|
|
return {
|
|
"title": self.title,
|
|
"main_speaker": self.speaker,
|
|
"speakers": ', '.join([x.name for x in self.speakers()]),
|
|
"additional_speakers": self.additional_speakers,
|
|
"kind": self.kind.name,
|
|
}
|
|
|
|
def __str__(self):
|
|
return self.title
|
|
|
|
|
|
reversion.register(ProposalBase)
|
|
|
|
|
|
class AdditionalSpeaker(models.Model):
|
|
|
|
SPEAKING_STATUS_PENDING = 1
|
|
SPEAKING_STATUS_ACCEPTED = 2
|
|
SPEAKING_STATUS_DECLINED = 3
|
|
|
|
SPEAKING_STATUS = [
|
|
(SPEAKING_STATUS_PENDING, _("Pending")),
|
|
(SPEAKING_STATUS_ACCEPTED, _("Accepted")),
|
|
(SPEAKING_STATUS_DECLINED, _("Declined")),
|
|
]
|
|
|
|
speaker = models.ForeignKey(Speaker, verbose_name=_("Speaker"))
|
|
proposalbase = models.ForeignKey(ProposalBase, verbose_name=_("Proposalbase"))
|
|
status = models.IntegerField(choices=SPEAKING_STATUS, default=SPEAKING_STATUS_PENDING, verbose_name=_("Status"))
|
|
|
|
class Meta:
|
|
unique_together = ("speaker", "proposalbase")
|
|
verbose_name = _("Addtional speaker")
|
|
verbose_name_plural = _("Additional speakers")
|
|
|
|
def __str__(self):
|
|
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
|
|
|
|
|
|
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):
|
|
|
|
proposal = models.ForeignKey(ProposalBase, related_name="supporting_documents", verbose_name=_("Proposal"))
|
|
|
|
uploaded_by = models.ForeignKey(User, verbose_name=_("Uploaded by"))
|
|
|
|
created_at = models.DateTimeField(default=now, verbose_name=_("Created at"))
|
|
|
|
file = models.FileField(upload_to=uuid_filename, verbose_name=_("File"))
|
|
description = models.CharField(max_length=140, verbose_name=_("Description"))
|
|
|
|
def download_url(self):
|
|
return reverse("proposal_document_download",
|
|
args=[self.pk, os.path.basename(self.file.name).lower()])
|