Changeset - 420d8ec870bd
[Not reviewed]
0 6 1
Scott Bragg - 8 years ago 2016-09-03 02:48:31
jsbragg@scriptforge.org
Remove description from Presentation, add fields to proposal for notification template.
7 files changed with 31 insertions and 8 deletions:
0 comments (0 inline, 0 general)
requirements/base.txt
Show inline comments
 
Django>=1.9.2
 
Django==1.9.2
 
django-appconf==1.0.1
 
django-model-utils==2.4.0
 
django-reversion==1.10.1
 
django-sitetree==1.5.1
 
django-taggit==0.18.0
 
django-timezone-field==1.3
 
django-user-accounts==1.3.1
 
easy-thumbnails==2.3
 
html5lib==0.9999999
 
markdown==2.6.5
 
pytz==2015.7
symposion/proposals/models.py
Show inline comments
...
 
@@ -7,254 +7,255 @@ from django.db import models
 
from django.db.models import Q
 
from django.utils.encoding import python_2_unicode_compatible
 
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.markdown_parser import parse
 
from symposion.conference.models import Section
 
from symposion.speakers.models import Speaker
 

	
 

	
 
@python_2_unicode_compatible
 
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
 

	
 

	
 
@python_2_unicode_compatible
 
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
 

	
 

	
 
@python_2_unicode_compatible
 
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. Edit using <a "
 
                    "href='http://warpedvisions.org/projects/"
 
                    "markdown-cheat-sheet/' target='_blank'>Markdown</a>.")
 
    )
 
    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. Edit using <a "
 
                    "href='http://warpedvisions.org/projects/"
 
                    "markdown-cheat-sheet/' target='_blank'>Markdown</a>.")
 
    )
 
    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. "
 
                    "Edit using <a "
 
                    "href='http://warpedvisions.org/projects/"
 
                    "markdown-cheat-sheet/' target='_blank'>Markdown</a>.")
 
    )
 
    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,
 
            "speaker": self.speaker.name,
 
            "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)
 

	
 

	
 
@python_2_unicode_compatible
 
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()])
symposion/reviews/models.py
Show inline comments
...
 
@@ -172,219 +172,218 @@ class Review(models.Model):
 
            # find the latest review
 
            latest = user_reviews.exclude(pk=self.pk).order_by("-submitted_at")[0]
 
        except IndexError:
 
            # did not find a latest which means this must be the only one.
 
            # treat it as a last, but delete the latest vote.
 
            self.proposal.result.update_vote(self.vote, removal=True)
 
            lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
 
            lv.delete()
 
        else:
 
            # handle that we've found a latest vote
 
            # check if self is the lastest vote
 
            if self == latest:
 
                # self is the latest review; revert the latest vote to the
 
                # previous vote
 
                previous = user_reviews.filter(submitted_at__lt=self.submitted_at)\
 
                    .order_by("-submitted_at")[0]
 
                self.proposal.result.update_vote(self.vote, previous=previous.vote, removal=True)
 
                lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
 
                lv.update(
 
                    vote=previous.vote,
 
                    submitted_at=previous.submitted_at,
 
                )
 
            else:
 
                # self is not the latest review so we just need to decrement
 
                # the comment count
 
                self.proposal.result.comment_count = models.F("comment_count") - 1
 
                self.proposal.result.save()
 
        # in all cases we need to delete the review; let's do it!
 
        super(Review, self).delete()
 

	
 
    def css_class(self):
 
        return {
 
            self.VOTES.ABSTAIN: "abstain",
 
            self.VOTES.PLUS_TWO: "plus-two",
 
            self.VOTES.PLUS_ONE: "plus-one",
 
            self.VOTES.MINUS_ONE: "minus-one",
 
            self.VOTES.MINUS_TWO: "minus-two",
 
        }[self.vote]
 

	
 
    @property
 
    def section(self):
 
        return self.proposal.kind.section.slug
 

	
 
    class Meta:
 
        verbose_name = _("review")
 
        verbose_name_plural = _("reviews")
 

	
 

	
 
class LatestVote(models.Model):
 
    VOTES = VOTES
 

	
 
    proposal = models.ForeignKey(ProposalBase, related_name="votes", verbose_name=_("Proposal"))
 
    user = models.ForeignKey(User, verbose_name=_("User"))
 

	
 
    # No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
 
    # like some complicated encoding system.
 
    vote = models.CharField(max_length=2, choices=VOTES.CHOICES, verbose_name=_("Vote"))
 
    submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))
 

	
 
    class Meta:
 
        unique_together = [("proposal", "user")]
 
        verbose_name = _("latest vote")
 
        verbose_name_plural = _("latest votes")
 

	
 
    def css_class(self):
 
        return {
 
            self.VOTES.ABSTAIN: "abstain",
 
            self.VOTES.PLUS_TWO: "plus-two",
 
            self.VOTES.PLUS_ONE: "plus-one",
 
            self.VOTES.MINUS_ONE: "minus-one",
 
            self.VOTES.MINUS_TWO: "minus-two",
 
        }[self.vote]
 

	
 

	
 
class ProposalResult(models.Model):
 
    proposal = models.OneToOneField(ProposalBase, related_name="result", verbose_name=_("Proposal"))
 
    score = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.00"), verbose_name=_("Score"))
 
    comment_count = models.PositiveIntegerField(default=0, verbose_name=_("Comment count"))
 
    # vote_count only counts non-abstain votes.
 
    vote_count = models.PositiveIntegerField(default=0, verbose_name=_("Vote count"))
 
    abstain = models.PositiveIntegerField(default=0, verbose_name=_("Abstain"))
 
    plus_two = models.PositiveIntegerField(default=0, verbose_name=_("Plus two"))
 
    plus_one = models.PositiveIntegerField(default=0, verbose_name=_("Plus one"))
 
    minus_one = models.PositiveIntegerField(default=0, verbose_name=_("Minus one"))
 
    minus_two = models.PositiveIntegerField(default=0, verbose_name=_("Minus two"))
 
    accepted = models.NullBooleanField(choices=[
 
        (True, "accepted"),
 
        (False, "rejected"),
 
        (None, "undecided"),
 
    ], default=None, verbose_name=_("Accepted"))
 
    status = models.CharField(max_length=20, choices=[
 
        ("accepted", _("accepted")),
 
        ("rejected", _("rejected")),
 
        ("undecided", _("undecided")),
 
        ("standby", _("standby")),
 
    ], default="undecided", verbose_name=_("Status"))
 

	
 
    @classmethod
 
    def full_calculate(cls):
 
        for proposal in ProposalBase.objects.all():
 
            result, created = cls._default_manager.get_or_create(proposal=proposal)
 
            result.update_vote()
 

	
 
    def update_vote(self, *a, **k):
 
        proposal = self.proposal
 
        self.comment_count = Review.objects.filter(proposal=proposal).count()
 
        agg = LatestVote.objects.filter(proposal=proposal).values(
 
            "vote"
 
        ).annotate(
 
            count=Count("vote")
 
        )
 
        vote_count = {}
 
        # Set the defaults
 
        for option in VOTES.CHOICES:
 
            vote_count[option[0]] = 0
 
        # Set the actual values if present
 
        for d in agg:
 
            vote_count[d["vote"]] = d["count"]
 

	
 
        self.abstain = vote_count[VOTES.ABSTAIN]
 
        self.plus_two = vote_count[VOTES.PLUS_TWO]
 
        self.plus_one = vote_count[VOTES.PLUS_ONE]
 
        self.minus_one = vote_count[VOTES.MINUS_ONE]
 
        self.minus_two = vote_count[VOTES.MINUS_TWO]
 
        self.vote_count = sum(i[1] for i in vote_count.items()) - self.abstain
 
        self.save()
 
        model = self.__class__
 
        model._default_manager.filter(pk=self.pk).update(score=score_expression())
 

	
 
    class Meta:
 
        verbose_name = _("proposal_result")
 
        verbose_name_plural = _("proposal_results")
 

	
 

	
 
class Comment(models.Model):
 
    proposal = models.ForeignKey(ProposalBase, related_name="comments", verbose_name=_("Proposal"))
 
    commenter = models.ForeignKey(User, verbose_name=_("Commenter"))
 
    text = models.TextField(verbose_name=_("Text"))
 
    text_html = models.TextField(blank=True)
 

	
 
    # Or perhaps more accurately, can the user see this comment.
 
    public = models.BooleanField(choices=[(True, _("public")), (False, _("private"))], default=False, verbose_name=_("Public"))
 
    commented_at = models.DateTimeField(default=datetime.now, verbose_name=_("Commented at"))
 

	
 
    class Meta:
 
        verbose_name = _("comment")
 
        verbose_name_plural = _("comments")
 

	
 
    def save(self, *args, **kwargs):
 
        self.text_html = parse(self.text)
 
        return super(Comment, self).save(*args, **kwargs)
 

	
 

	
 
class NotificationTemplate(models.Model):
 

	
 
    label = models.CharField(max_length=100, verbose_name=_("Label"))
 
    from_address = models.EmailField(verbose_name=_("From address"))
 
    subject = models.CharField(max_length=100, verbose_name=_("Subject"))
 
    body = models.TextField(verbose_name=_("Body"))
 

	
 
    class Meta:
 
        verbose_name = _("notification template")
 
        verbose_name_plural = _("notification templates")
 

	
 

	
 
class ResultNotification(models.Model):
 

	
 
    proposal = models.ForeignKey(ProposalBase, related_name="notifications", verbose_name=_("Proposal"))
 
    template = models.ForeignKey(NotificationTemplate, null=True, blank=True,
 
                                 on_delete=models.SET_NULL, verbose_name=_("Template"))
 
    timestamp = models.DateTimeField(default=datetime.now, verbose_name=_("Timestamp"))
 
    to_address = models.EmailField(verbose_name=_("To address"))
 
    from_address = models.EmailField(verbose_name=_("From address"))
 
    subject = models.CharField(max_length=100, verbose_name=_("Subject"))
 
    body = models.TextField(verbose_name=_("Body"))
 

	
 
    def recipients(self):
 
        for speaker in self.proposal.speakers():
 
            yield speaker.email
 

	
 
    @property
 
    def email_args(self):
 
        return (self.subject, self.body, self.from_address, self.recipients())
 

	
 

	
 
def promote_proposal(proposal):
 
    if hasattr(proposal, "presentation") and proposal.presentation:
 
        # already promoted
 
        presentation = proposal.presentation
 
    else:
 
        presentation = Presentation(
 
            title=proposal.title,
 
            description=proposal.description,
 
            abstract=proposal.abstract,
 
            speaker=proposal.speaker,
 
            section=proposal.section,
 
            proposal_base=proposal,
 
        )
 
        presentation.save()
 
        for speaker in proposal.additional_speakers.all():
 
            presentation.additional_speakers.add(speaker)
 
            presentation.save()
 

	
 
    return presentation
 

	
 

	
 
def unpromote_proposal(proposal):
 
    if hasattr(proposal, "presentation") and proposal.presentation:
 
        proposal.presentation.delete()
 

	
 

	
 
def accepted_proposal(sender, instance=None, **kwargs):
 
    if instance is None:
 
        return
 
    if instance.status == "accepted":
 
        promote_proposal(instance.proposal)
 
    else:
 
        unpromote_proposal(instance.proposal)
 
post_save.connect(accepted_proposal, sender=ProposalResult)
symposion/reviews/views.py
Show inline comments
...
 
@@ -456,204 +456,208 @@ def review_detail(request, pk):
 

	
 

	
 
@login_required
 
@require_POST
 
def review_delete(request, pk):
 
    review = get_object_or_404(Review, pk=pk)
 
    section_slug = review.section
 

	
 
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
 
        return access_not_permitted(request)
 

	
 
    review = get_object_or_404(Review, pk=pk)
 
    review.delete()
 

	
 
    return redirect("review_detail", pk=review.proposal.pk)
 

	
 

	
 
@login_required
 
def review_status(request, section_slug=None, key=None):
 

	
 
    if not request.user.has_perm("reviews.can_review_%s" % section_slug):
 
        return access_not_permitted(request)
 

	
 
    ctx = {
 
        "section_slug": section_slug,
 
        "vote_threshold": VOTE_THRESHOLD,
 
    }
 

	
 
    queryset = ProposalBase.objects.select_related("speaker__user", "result").select_subclasses()
 
    if section_slug:
 
        queryset = queryset.filter(kind__section__slug=section_slug)
 

	
 
    proposals = dict((key, filt(queryset)) for key, filt in REVIEW_STATUS_FILTERS.items())
 

	
 
    admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)
 

	
 
    for status in proposals:
 
        if key and key != status:
 
            continue
 
        proposals[status] = list(proposals_generator(request, proposals[status], check_speaker=not admin))
 

	
 
    if key:
 
        ctx.update({
 
            "key": key,
 
            "proposals": proposals[key],
 
        })
 
    else:
 
        ctx["proposals"] = proposals
 

	
 
    return render(request, "symposion/reviews/review_stats.html", ctx)
 

	
 

	
 
@login_required
 
def review_assignments(request):
 
    if not request.user.groups.filter(name="reviewers").exists():
 
        return access_not_permitted(request)
 
    assignments = ReviewAssignment.objects.filter(
 
        user=request.user,
 
        opted_out=False
 
    )
 
    return render(request, "symposion/reviews/review_assignment.html", {
 
        "assignments": assignments,
 
    })
 

	
 

	
 
@login_required
 
@require_POST
 
def review_assignment_opt_out(request, pk):
 
    review_assignment = get_object_or_404(
 
        ReviewAssignment, pk=pk, user=request.user)
 
    if not review_assignment.opted_out:
 
        review_assignment.opted_out = True
 
        review_assignment.save()
 
        ReviewAssignment.create_assignments(
 
            review_assignment.proposal, origin=ReviewAssignment.AUTO_ASSIGNED_LATER)
 
    return redirect("review_assignments")
 

	
 

	
 
@login_required
 
def review_bulk_accept(request, section_slug):
 
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
 
        return access_not_permitted(request)
 
    if request.method == "POST":
 
        form = BulkPresentationForm(request.POST)
 
        if form.is_valid():
 
            talk_ids = form.cleaned_data["talk_ids"].split(",")
 
            talks = ProposalBase.objects.filter(id__in=talk_ids).select_related("result")
 
            for talk in talks:
 
                talk.result.status = "accepted"
 
                talk.result.save()
 
            return redirect("review_section", section_slug=section_slug)
 
    else:
 
        form = BulkPresentationForm()
 

	
 
    return render(request, "symposion/reviews/review_bulk_accept.html", {
 
        "form": form,
 
    })
 

	
 

	
 
@login_required
 
def result_notification(request, section_slug, status):
 
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
 
        return access_not_permitted(request)
 

	
 
    proposals = ProposalBase.objects.filter(kind__section__slug=section_slug, result__status=status).select_related("speaker__user", "result").select_subclasses()
 
    notification_templates = NotificationTemplate.objects.all()
 

	
 
    ctx = {
 
        "section_slug": section_slug,
 
        "status": status,
 
        "proposals": proposals,
 
        "notification_templates": notification_templates,
 
    }
 
    return render(request, "symposion/reviews/result_notification.html", ctx)
 

	
 

	
 
@login_required
 
def result_notification_prepare(request, section_slug, status):
 
    if request.method != "POST":
 
        return HttpResponseNotAllowed(["POST"])
 

	
 
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
 
        return access_not_permitted(request)
 

	
 
    proposal_pks = []
 
    try:
 
        for pk in request.POST.getlist("_selected_action"):
 
            proposal_pks.append(int(pk))
 
    except ValueError:
 
        return HttpResponseBadRequest()
 
    proposals = ProposalBase.objects.filter(
 
        kind__section__slug=section_slug,
 
        result__status=status,
 
    )
 
    proposals = proposals.filter(pk__in=proposal_pks)
 
    proposals = proposals.select_related("speaker__user", "result")
 
    proposals = proposals.select_subclasses()
 

	
 
    notification_template_pk = request.POST.get("notification_template", "")
 
    if notification_template_pk:
 
        notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
 
    else:
 
        notification_template = None
 

	
 
    ctx = {
 
        "section_slug": section_slug,
 
        "status": status,
 
        "notification_template": notification_template,
 
        "proposals": proposals,
 
        "proposal_pks": ",".join([str(pk) for pk in proposal_pks]),
 
    }
 
    return render(request, "symposion/reviews/result_notification_prepare.html", ctx)
 

	
 

	
 
@login_required
 
def result_notification_send(request, section_slug, status):
 
    if request.method != "POST":
 
        return HttpResponseNotAllowed(["POST"])
 

	
 
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
 
        return access_not_permitted(request)
 

	
 
    if not all([k in request.POST for k in ["proposal_pks", "from_address", "subject", "body"]]):
 
        return HttpResponseBadRequest()
 

	
 
    try:
 
        proposal_pks = [int(pk) for pk in request.POST["proposal_pks"].split(",")]
 
    except ValueError:
 
        return HttpResponseBadRequest()
 

	
 
    proposals = ProposalBase.objects.filter(
 
        kind__section__slug=section_slug,
 
        result__status=status,
 
    )
 
    proposals = proposals.filter(pk__in=proposal_pks)
 
    proposals = proposals.select_related("speaker__user", "result")
 
    proposals = proposals.select_subclasses()
 

	
 
    notification_template_pk = request.POST.get("notification_template", "")
 
    if notification_template_pk:
 
        notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
 
    else:
 
        notification_template = None
 

	
 
    emails = []
 

	
 
    for proposal in proposals:
 
        rn = ResultNotification()
 
        rn.proposal = proposal
 
        rn.template = notification_template
 
        rn.to_address = proposal.speaker_email
 
        rn.from_address = request.POST["from_address"]
 
        rn.subject = request.POST["subject"]
 
        rn.subject = Template(request.POST["subject"]).render(
 
            Context({
 
                "proposal": proposal.notification_email_context()
 
            })
 
        )
 
        rn.body = Template(request.POST["body"]).render(
 
            Context({
 
                "proposal": proposal.notification_email_context()
 
            })
 
        )
 
        rn.save()
 
        emails.append(rn.email_args)
 

	
 
    send_mass_mail(emails)
 

	
 
    return redirect("result_notification", section_slug=section_slug, status=status)
symposion/schedule/migrations/0002_auto_20160903_0146.py
Show inline comments
 
new file 100644
 
# -*- coding: utf-8 -*-
 
# Generated by Django 1.9.2 on 2016-09-03 01:46
 
from __future__ import unicode_literals
 

	
 
from django.db import migrations, models
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('symposion_schedule', '0001_initial'),
 
    ]
 

	
 
    operations = [
 
        migrations.RemoveField(
 
            model_name='presentation',
 
            name='description',
 
        ),
 
        migrations.RemoveField(
 
            model_name='presentation',
 
            name='description_html',
 
        ),
 
    ]
symposion/schedule/models.py
Show inline comments
 
from __future__ import unicode_literals
 

	
 
import datetime
 

	
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.contrib.auth.models import User
 
from django.db import models
 
from django.utils.encoding import python_2_unicode_compatible
 
from django.utils.translation import ugettext_lazy as _
 

	
 
from symposion.markdown_parser import parse
 
from symposion.proposals.models import ProposalBase
 
from symposion.conference.models import Section
 
from symposion.speakers.models import Speaker
 

	
 

	
 
@python_2_unicode_compatible
 
class Schedule(models.Model):
 

	
 
    section = models.OneToOneField(Section, verbose_name=_("Section"))
 
    published = models.BooleanField(default=True, verbose_name=_("Published"))
 
    hidden = models.BooleanField(_("Hide schedule from overall conference view"), default=False)
 

	
 
    def __str__(self):
 
        return "%s Schedule" % self.section
 

	
 
    class Meta:
 
        ordering = ["section"]
 
        verbose_name = _('Schedule')
 
        verbose_name_plural = _('Schedules')
 

	
 

	
 
@python_2_unicode_compatible
 
class Day(models.Model):
 

	
 
    schedule = models.ForeignKey(Schedule, verbose_name=_("Schedule"))
 
    date = models.DateField(verbose_name=_("Date"))
 

	
 
    def __str__(self):
 
        return "%s" % self.date
 

	
 
    class Meta:
 
        unique_together = [("schedule", "date")]
 
        ordering = ["date"]
 
        verbose_name = _("date")
 
        verbose_name_plural = _("dates")
 

	
 

	
 
@python_2_unicode_compatible
 
class Room(models.Model):
 

	
 
    schedule = models.ForeignKey(Schedule, verbose_name=_("Schedule"))
 
    name = models.CharField(max_length=65, verbose_name=_("Name"))
 
    order = models.PositiveIntegerField(verbose_name=_("Order"))
 

	
 
    def __str__(self):
 
        return self.name
 

	
 
    class Meta:
 
        verbose_name = _("Room")
 
        verbose_name_plural = _("Rooms")
 

	
 

	
 
@python_2_unicode_compatible
 
class SlotKind(models.Model):
 
    """
 
    A slot kind represents what kind a slot is. For example, a slot can be a
 
    break, lunch, or X-minute talk.
 
    """
 

	
 
    schedule = models.ForeignKey(Schedule, verbose_name=_("schedule"))
 
    label = models.CharField(max_length=50, verbose_name=_("Label"))
 

	
 
    def __str__(self):
 
        return self.label
 

	
 
    class Meta:
 
        verbose_name = _("Slot kind")
 
        verbose_name_plural = _("Slot kinds")
 

	
 

	
 
@python_2_unicode_compatible
 
class Slot(models.Model):
 

	
 
    name = models.CharField(max_length=100, editable=False)
 
    day = models.ForeignKey(Day, verbose_name=_("Day"))
 
    kind = models.ForeignKey(SlotKind, verbose_name=_("Kind"))
 
    start = models.TimeField(verbose_name=_("Start"))
 
    end = models.TimeField(verbose_name=_("End"))
 
    content_override = models.TextField(blank=True, verbose_name=_("Content override"))
 
    content_override_html = models.TextField(blank=True)
 

	
 
    def assign(self, content):
 
        """
 
        Assign the given content to this slot and if a previous slot content
 
        was given we need to unlink it to avoid integrity errors.
 
        """
 
        self.unassign()
 
        content.slot = self
 
        content.save()
 

	
 
    def unassign(self):
 
        """
 
        Unassign the associated content with this slot.
 
        """
 
        content = self.content
 
        if content and content.slot_id:
 
            content.slot = None
 
            content.save()
 

	
 
    @property
 
    def content(self):
 
        """
 
        Return the content this slot represents.
 
        @@@ hard-coded for presentation for now
 
        """
 
        try:
 
            return self.content_ptr
 
        except ObjectDoesNotExist:
 
            return None
 

	
 
    @property
 
    def start_datetime(self):
 
        return datetime.datetime(
 
            self.day.date.year,
 
            self.day.date.month,
 
            self.day.date.day,
 
            self.start.hour,
 
            self.start.minute)
 

	
 
    @property
 
    def end_datetime(self):
 
        return datetime.datetime(
 
            self.day.date.year,
 
            self.day.date.month,
 
            self.day.date.day,
 
            self.end.hour,
 
            self.end.minute)
 

	
 
    @property
 
    def length_in_minutes(self):
 
        return int(
 
            (self.end_datetime - self.start_datetime).total_seconds() / 60)
 

	
 
    @property
 
    def rooms(self):
 
        return Room.objects.filter(pk__in=self.slotroom_set.values("room"))
 

	
 
    def save(self, *args, **kwargs):
 
        roomlist = ' '.join(map(lambda r: r.__unicode__(), self.rooms))
 
        self.name = "%s %s (%s - %s) %s" % (self.day, self.kind, self.start, self.end, roomlist)
 
        self.content_override_html = parse(self.content_override)
 
        super(Slot, self).save(*args, **kwargs)
 

	
 
    def __str__(self):
 
        return self.name
 

	
 
    class Meta:
 
        ordering = ["day", "start", "end"]
 
        verbose_name = _("slot")
 
        verbose_name_plural = _("slots")
 

	
 

	
 
@python_2_unicode_compatible
 
class SlotRoom(models.Model):
 
    """
 
    Links a slot with a room.
 
    """
 

	
 
    slot = models.ForeignKey(Slot, verbose_name=_("Slot"))
 
    room = models.ForeignKey(Room, verbose_name=_("Room"))
 

	
 
    def __str__(self):
 
        return "%s %s" % (self.room, self.slot)
 

	
 
    class Meta:
 
        unique_together = [("slot", "room")]
 
        ordering = ["slot", "room__order"]
 
        verbose_name = _("Slot room")
 
        verbose_name_plural = _("Slot rooms")
 

	
 

	
 
@python_2_unicode_compatible
 
class Presentation(models.Model):
 

	
 
    slot = models.OneToOneField(Slot, null=True, blank=True, related_name="content_ptr", verbose_name=_("Slot"))
 
    title = models.CharField(max_length=100, verbose_name=_("Title"))
 
    description = models.TextField(verbose_name=_("Description"))
 
    description_html = models.TextField(blank=True)
 
    abstract = models.TextField(verbose_name=_("Abstract"))
 
    abstract_html = models.TextField(blank=True)
 
    speaker = models.ForeignKey(Speaker, related_name="presentations", verbose_name=_("Speaker"))
 
    additional_speakers = models.ManyToManyField(Speaker, related_name="copresentations",
 
                                                 blank=True, verbose_name=_("Additional speakers"))
 
    cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled"))
 
    proposal_base = models.OneToOneField(ProposalBase, related_name="presentation", verbose_name=_("Proposal base"))
 
    section = models.ForeignKey(Section, related_name="presentations", verbose_name=_("Section"))
 

	
 
    def save(self, *args, **kwargs):
 
        self.description_html = parse(self.description)
 
        self.abstract_html = parse(self.abstract)
 
        return super(Presentation, self).save(*args, **kwargs)
 

	
 
    @property
 
    def number(self):
 
        return self.proposal.number
 

	
 
    @property
 
    def proposal(self):
 
        if self.proposal_base_id is None:
 
            return None
 
        return ProposalBase.objects.get_subclass(pk=self.proposal_base_id)
 

	
 
    def speakers(self):
 
        yield self.speaker
 
        for speaker in self.additional_speakers.all():
 
            if speaker.user:
 
                yield speaker
 

	
 
    def __str__(self):
 
        return "#%s %s (%s)" % (self.number, self.title, self.speaker)
 

	
 
    class Meta:
 
        ordering = ["slot"]
 
        verbose_name = _("presentation")
 
        verbose_name_plural = _("presentations")
 

	
 

	
 
@python_2_unicode_compatible
 
class Session(models.Model):
 

	
 
    day = models.ForeignKey(Day, related_name="sessions", verbose_name=_("Day"))
 
    slots = models.ManyToManyField(Slot, related_name="sessions", verbose_name=_("Slots"))
 

	
 
    def sorted_slots(self):
 
        return self.slots.order_by("start")
 

	
 
    def start(self):
 
        slots = self.sorted_slots()
 
        if slots:
 
            return list(slots)[0].start
 
        else:
 
            return None
 

	
 
    def end(self):
 
        slots = self.sorted_slots()
 
        if slots:
 
            return list(slots)[-1].end
 
        else:
 
            return None
 

	
 
    def __str__(self):
 
        start = self.start()
 
        end = self.end()
 
        if start and end:
 
            return "%s: %s - %s" % (
 
                self.day.date.strftime("%a"),
 
                start.strftime("%X"),
 
                end.strftime("%X")
 
            )
 
        return ""
 

	
 
    class Meta:
 
        verbose_name = _("Session")
 
        verbose_name_plural = _("Sessions")
 

	
 

	
 
@python_2_unicode_compatible
 
class SessionRole(models.Model):
 

	
 
    SESSION_ROLE_CHAIR = 1
 
    SESSION_ROLE_RUNNER = 2
 

	
 
    SESSION_ROLE_TYPES = [
 
        (SESSION_ROLE_CHAIR, _("Session Chair")),
 
        (SESSION_ROLE_RUNNER, _("Session Runner")),
 
    ]
 

	
 
    session = models.ForeignKey(Session, verbose_name=_("Session"))
 
    user = models.ForeignKey(User, verbose_name=_("User"))
 
    role = models.IntegerField(choices=SESSION_ROLE_TYPES, verbose_name=_("Role"))
 
    status = models.NullBooleanField(verbose_name=_("Status"))
 

	
 
    submitted = models.DateTimeField(default=datetime.datetime.now)
 

	
 
    class Meta:
 
        unique_together = [("session", "user", "role")]
 
        verbose_name = _("Session role")
 
        verbose_name_plural = _("Session roles")
 

	
 
    def __str__(self):
 
        return "%s %s: %s" % (self.user, self.session,
 
                              self.SESSION_ROLE_TYPES[self.role - 1][1])
symposion/schedule/views.py
Show inline comments
...
 
@@ -36,290 +36,289 @@ def schedule_conference(request):
 

	
 
    if request.user.is_staff:
 
        schedules = Schedule.objects.filter(hidden=False)
 
    else:
 
        schedules = Schedule.objects.filter(published=True, hidden=False)
 

	
 
    sections = []
 
    for schedule in schedules:
 
        days_qs = Day.objects.filter(schedule=schedule)
 
        days = [TimeTable(day) for day in days_qs]
 
        sections.append({
 
            "schedule": schedule,
 
            "days": days,
 
        })
 

	
 
    ctx = {
 
        "sections": sections,
 
    }
 
    return render(request, "symposion/schedule/schedule_conference.html", ctx)
 

	
 

	
 
def schedule_detail(request, slug=None):
 

	
 
    schedule = fetch_schedule(slug)
 
    if not schedule.published and not request.user.is_staff:
 
        raise Http404()
 

	
 
    days_qs = Day.objects.filter(schedule=schedule)
 
    days = [TimeTable(day) for day in days_qs]
 

	
 
    ctx = {
 
        "schedule": schedule,
 
        "days": days,
 
    }
 
    return render(request, "symposion/schedule/schedule_detail.html", ctx)
 

	
 

	
 
def schedule_list(request, slug=None):
 
    schedule = fetch_schedule(slug)
 
    if not schedule.published and not request.user.is_staff:
 
        raise Http404()
 

	
 
    presentations = Presentation.objects.filter(section=schedule.section)
 
    presentations = presentations.exclude(cancelled=True)
 

	
 
    ctx = {
 
        "schedule": schedule,
 
        "presentations": presentations,
 
    }
 
    return render(request, "symposion/schedule/schedule_list.html", ctx)
 

	
 

	
 
def schedule_list_csv(request, slug=None):
 
    schedule = fetch_schedule(slug)
 
    if not schedule.published and not request.user.is_staff:
 
        raise Http404()
 

	
 
    presentations = Presentation.objects.filter(section=schedule.section)
 
    presentations = presentations.exclude(cancelled=True).order_by("id")
 
    response = HttpResponse(content_type="text/csv")
 

	
 
    if slug:
 
        file_slug = slug
 
    else:
 
        file_slug = "presentations"
 
    response["Content-Disposition"] = 'attachment; filename="%s.csv"' % file_slug
 

	
 
    response.write(loader.get_template("symposion/schedule/schedule_list.csv").render(Context({
 
        "presentations": presentations,
 

	
 
    })))
 
    return response
 

	
 

	
 
@login_required
 
def schedule_edit(request, slug=None):
 

	
 
    if not request.user.is_staff:
 
        raise Http404()
 

	
 
    schedule = fetch_schedule(slug)
 

	
 
    if request.method == "POST":
 
        form = ScheduleSectionForm(
 
            request.POST, request.FILES, schedule=schedule
 
        )
 
        if form.is_valid():
 
            if 'submit' in form.data:
 
                msg = form.build_schedule()
 
            elif 'delete' in form.data:
 
                msg = form.delete_schedule()
 
            messages.add_message(request, msg[0], msg[1])
 
    else:
 
        form = ScheduleSectionForm(schedule=schedule)
 
    days_qs = Day.objects.filter(schedule=schedule)
 
    days = [TimeTable(day) for day in days_qs]
 
    ctx = {
 
        "schedule": schedule,
 
        "days": days,
 
        "form": form
 
    }
 
    return render(request, "symposion/schedule/schedule_edit.html", ctx)
 

	
 

	
 
@login_required
 
def schedule_slot_edit(request, slug, slot_pk):
 

	
 
    if not request.user.is_staff:
 
        raise Http404()
 

	
 
    slot = get_object_or_404(Slot, day__schedule__section__slug=slug, pk=slot_pk)
 

	
 
    if request.method == "POST":
 
        form = SlotEditForm(request.POST, slot=slot)
 
        if form.is_valid():
 
            save = False
 
            if "content_override" in form.cleaned_data:
 
                slot.content_override = form.cleaned_data["content_override"]
 
                save = True
 
            if "presentation" in form.cleaned_data:
 
                presentation = form.cleaned_data["presentation"]
 
                if presentation is None:
 
                    slot.unassign()
 
                else:
 
                    slot.assign(presentation)
 
            if save:
 
                slot.save()
 
        return redirect("schedule_edit", slug)
 
    else:
 
        form = SlotEditForm(slot=slot)
 
        ctx = {
 
            "slug": slug,
 
            "form": form,
 
            "slot": slot,
 
        }
 
        return render(request, "symposion/schedule/_slot_edit.html", ctx)
 

	
 

	
 
def schedule_presentation_detail(request, pk):
 

	
 
    presentation = get_object_or_404(Presentation, pk=pk)
 
    if presentation.slot:
 
        schedule = presentation.slot.day.schedule
 
        if not schedule.published and not request.user.is_staff:
 
            raise Http404()
 
    else:
 
        schedule = None
 

	
 
    ctx = {
 
        "presentation": presentation,
 
        "schedule": schedule,
 
    }
 
    return render(request, "symposion/schedule/presentation_detail.html", ctx)
 

	
 

	
 
def schedule_json(request):
 
    slots = Slot.objects.filter(
 
        day__schedule__published=True,
 
        day__schedule__hidden=False
 
    ).order_by("start")
 

	
 
    protocol = request.META.get('HTTP_X_FORWARDED_PROTO', 'http')
 
    data = []
 
    for slot in slots:
 
        slot_data = {
 
            "room": ", ".join(room["name"] for room in slot.rooms.values()),
 
            "rooms": [room["name"] for room in slot.rooms.values()],
 
            "start": slot.start_datetime.isoformat(),
 
            "end": slot.end_datetime.isoformat(),
 
            "duration": slot.length_in_minutes,
 
            "kind": slot.kind.label,
 
            "section": slot.day.schedule.section.slug,
 
            "conf_key": slot.pk,
 
            # TODO: models should be changed.
 
            # these are model features from other conferences that have forked symposion
 
            # these have been used almost everywhere and are good candidates for
 
            # base proposals
 
            "license": "CC BY",
 
            "tags": "",
 
            "released": True,
 
            "contact": [],
 

	
 

	
 
        }
 
        if hasattr(slot.content, "proposal"):
 
            slot_data.update({
 
                "name": slot.content.title,
 
                "authors": [s.name for s in slot.content.speakers()],
 
                "contact": [
 
                    s.email for s in slot.content.speakers()
 
                ] if request.user.is_staff else ["redacted"],
 
                "abstract": slot.content.abstract.raw,
 
                "description": slot.content.description.raw,
 
                "conf_url": "%s://%s%s" % (
 
                    protocol,
 
                    Site.objects.get_current().domain,
 
                    reverse("schedule_presentation_detail", args=[slot.content.pk])
 
                ),
 
                "cancelled": slot.content.cancelled,
 
            })
 
        else:
 
            slot_data.update({
 
                "name": slot.content_override.raw if slot.content_override else "Slot",
 
            })
 
        data.append(slot_data)
 

	
 
    return HttpResponse(
 
        json.dumps({"schedule": data}),
 
        content_type="application/json"
 
    )
 

	
 

	
 
def session_list(request):
 
    sessions = Session.objects.all().order_by('pk')
 

	
 
    return render(request, "symposion/schedule/session_list.html", {
 
        "sessions": sessions,
 
    })
 

	
 

	
 
@login_required
 
def session_staff_email(request):
 

	
 
    if not request.user.is_staff:
 
        return redirect("schedule_session_list")
 

	
 
    data = "\n".join(user.email for user in User.objects.filter(sessionrole__isnull=False).distinct())
 

	
 
    return HttpResponse(data, content_type="text/plain;charset=UTF-8")
 

	
 

	
 
def session_detail(request, session_id):
 

	
 
    session = get_object_or_404(Session, id=session_id)
 

	
 
    chair = None
 
    chair_denied = False
 
    chairs = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_CHAIR).exclude(status=False)
 
    if chairs:
 
        chair = chairs[0].user
 
    else:
 
        if request.user.is_authenticated():
 
            # did the current user previously try to apply and got rejected?
 
            if SessionRole.objects.filter(session=session, user=request.user, role=SessionRole.SESSION_ROLE_CHAIR, status=False):
 
                chair_denied = True
 

	
 
    runner = None
 
    runner_denied = False
 
    runners = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_RUNNER).exclude(status=False)
 
    if runners:
 
        runner = runners[0].user
 
    else:
 
        if request.user.is_authenticated():
 
            # did the current user previously try to apply and got rejected?
 
            if SessionRole.objects.filter(session=session, user=request.user, role=SessionRole.SESSION_ROLE_RUNNER, status=False):
 
                runner_denied = True
 

	
 
    if request.method == "POST" and request.user.is_authenticated():
 
        if not hasattr(request.user, "profile") or not request.user.profile.is_complete:
 
            response = redirect("profile_edit")
 
            response["Location"] += "?next=%s" % request.path
 
            return response
 

	
 
        role = request.POST.get("role")
 
        if role == "chair":
 
            if chair is None and not chair_denied:
 
                SessionRole(session=session, role=SessionRole.SESSION_ROLE_CHAIR, user=request.user).save()
 
        elif role == "runner":
 
            if runner is None and not runner_denied:
 
                SessionRole(session=session, role=SessionRole.SESSION_ROLE_RUNNER, user=request.user).save()
 
        elif role == "un-chair":
 
            if chair == request.user:
 
                session_role = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_CHAIR, user=request.user)
 
                if session_role:
 
                    session_role[0].delete()
 
        elif role == "un-runner":
 
            if runner == request.user:
 
                session_role = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_RUNNER, user=request.user)
 
                if session_role:
 
                    session_role[0].delete()
 

	
 
        return redirect("schedule_session_detail", session_id)
 

	
 
    return render(request, "symposion/schedule/session_detail.html", {
 
        "session": session,
 
        "chair": chair,
 
        "chair_denied": chair_denied,
 
        "runner": runner,
 
        "runner_denied": runner_denied,
 
    })
0 comments (0 inline, 0 general)