add proposals app from pycon
This commit is contained in:
parent
2b7f5546a0
commit
7596922f4d
10 changed files with 697 additions and 0 deletions
0
symposion/proposals/__init__.py
Normal file
0
symposion/proposals/__init__.py
Normal file
36
symposion/proposals/actions.py
Normal file
36
symposion/proposals/actions.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import csv
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
|
||||||
|
def export_as_csv_action(description="Export selected objects as CSV file",
|
||||||
|
fields=None, exclude=None, header=True):
|
||||||
|
"""
|
||||||
|
This function returns an export csv action
|
||||||
|
'fields' and 'exclude' work like in django ModelForm
|
||||||
|
'header' is whether or not to output the column names as the first row
|
||||||
|
"""
|
||||||
|
def export_as_csv(modeladmin, request, queryset):
|
||||||
|
"""
|
||||||
|
Generic csv export admin action.
|
||||||
|
based on http://djangosnippets.org/snippets/1697/
|
||||||
|
"""
|
||||||
|
opts = modeladmin.model._meta
|
||||||
|
if fields:
|
||||||
|
fieldset = set(fields)
|
||||||
|
field_names = fieldset
|
||||||
|
elif exclude:
|
||||||
|
excludeset = set(exclude)
|
||||||
|
field_names = field_names - excludeset
|
||||||
|
|
||||||
|
response = HttpResponse(mimetype='text/csv')
|
||||||
|
response['Content-Disposition'] = 'attachment; filename=%s.csv' % unicode(opts).replace('.', '_')
|
||||||
|
|
||||||
|
writer = csv.writer(response)
|
||||||
|
if header:
|
||||||
|
writer.writerow(list(field_names))
|
||||||
|
for obj in queryset:
|
||||||
|
writer.writerow([unicode(getattr(obj, field)).encode("utf-8", "replace") for field in field_names])
|
||||||
|
return response
|
||||||
|
export_as_csv.short_description = description
|
||||||
|
return export_as_csv
|
32
symposion/proposals/admin.py
Normal file
32
symposion/proposals/admin.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from django.contrib import admin
|
||||||
|
|
||||||
|
# from symposion.proposals.actions import export_as_csv_action
|
||||||
|
from symposion.proposals.models import ProposalSection, ProposalKind
|
||||||
|
|
||||||
|
|
||||||
|
# admin.site.register(Proposal,
|
||||||
|
# list_display = [
|
||||||
|
# "id",
|
||||||
|
# "title",
|
||||||
|
# "speaker",
|
||||||
|
# "speaker_email",
|
||||||
|
# "kind",
|
||||||
|
# "audience_level",
|
||||||
|
# "cancelled",
|
||||||
|
# ],
|
||||||
|
# list_filter = [
|
||||||
|
# "kind__name",
|
||||||
|
# "result__accepted",
|
||||||
|
# ],
|
||||||
|
# actions = [export_as_csv_action("CSV Export", fields=[
|
||||||
|
# "id",
|
||||||
|
# "title",
|
||||||
|
# "speaker",
|
||||||
|
# "speaker_email",
|
||||||
|
# "kind",
|
||||||
|
# ])]
|
||||||
|
# )
|
||||||
|
|
||||||
|
|
||||||
|
admin.site.register(ProposalSection)
|
||||||
|
admin.site.register(ProposalKind)
|
41
symposion/proposals/forms.py
Normal file
41
symposion/proposals/forms.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from django import forms
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from symposion.proposals.models import SupportingDocument
|
||||||
|
# from markitup.widgets import MarkItUpWidget
|
||||||
|
|
||||||
|
|
||||||
|
# @@@ generic proposal form
|
||||||
|
|
||||||
|
|
||||||
|
class AddSpeakerForm(forms.Form):
|
||||||
|
|
||||||
|
email = forms.EmailField(
|
||||||
|
label="Email address of new speaker (use their email address, not yours)"
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.proposal = kwargs.pop("proposal")
|
||||||
|
super(AddSpeakerForm, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
value = self.cleaned_data["email"]
|
||||||
|
exists = self.proposal.additional_speakers.filter(
|
||||||
|
Q(user=None, invite_email=value) |
|
||||||
|
Q(user__email=value)
|
||||||
|
).exists()
|
||||||
|
if exists:
|
||||||
|
raise forms.ValidationError(
|
||||||
|
"This email address has already been invited to your talk proposal"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class SupportingDocumentCreateForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = SupportingDocument
|
||||||
|
fields = [
|
||||||
|
"file",
|
||||||
|
"description",
|
||||||
|
]
|
27
symposion/proposals/managers.py
Normal file
27
symposion/proposals/managers.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models.query import QuerySet
|
||||||
|
|
||||||
|
|
||||||
|
class CachingM2MQuerySet(QuerySet):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(CachingM2MQuerySet, self).__init__(*args, **kwargs)
|
||||||
|
self.cached_m2m_field = kwargs["m2m_field"]
|
||||||
|
|
||||||
|
def iterator(self):
|
||||||
|
parent_iter = super(CachingM2MQuerySet, self).iterator()
|
||||||
|
m2m_model = getattr(self.model, self.cached_m2m_field).through
|
||||||
|
|
||||||
|
for obj in parent_iter:
|
||||||
|
if obj.id in cached_objects:
|
||||||
|
setattr(obj, "_cached_m2m_%s" % self.cached_m2m_field)
|
||||||
|
yield obj
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalManager(models.Manager):
|
||||||
|
def cache_m2m(self, m2m_field):
|
||||||
|
return CachingM2MQuerySet(self.model, using=self._db, m2m_field=m2m_field)
|
||||||
|
AdditionalSpeaker = queryset.model.additional_speakers.through
|
||||||
|
additional_speakers = collections.defaultdict(set)
|
||||||
|
for additional_speaker in AdditionalSpeaker._default_manager.filter(proposal__in=queryset).select_related("speaker__user"):
|
||||||
|
additional_speakers[additional_speaker.proposal_id].add(additional_speaker.speaker)
|
146
symposion/proposals/models.py
Normal file
146
symposion/proposals/models.py
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
import datetime
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.db import models
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
from markitup.fields import MarkupField
|
||||||
|
|
||||||
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
|
from symposion.conference.models import Section
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
start = models.DateTimeField(null=True, blank=True)
|
||||||
|
end = models.DateTimeField(null=True, blank=True)
|
||||||
|
closed = models.NullBooleanField()
|
||||||
|
published = models.NullBooleanField()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def available(cls):
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
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 __unicode__(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")
|
||||||
|
|
||||||
|
name = models.CharField("name", max_length=100)
|
||||||
|
slug = models.SlugField()
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class ProposalBase(models.Model):
|
||||||
|
|
||||||
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
kind = models.ForeignKey(ProposalKind)
|
||||||
|
|
||||||
|
title = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(
|
||||||
|
max_length=400, # @@@ need to enforce 400 in UI
|
||||||
|
help_text="If your talk is accepted this will be made public and printed in the program. Should be one paragraph, maximum 400 characters."
|
||||||
|
)
|
||||||
|
abstract = MarkupField(
|
||||||
|
help_text="Detailed description and outline. Will be made public if your talk is accepted. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>."
|
||||||
|
)
|
||||||
|
additional_notes = MarkupField(
|
||||||
|
blank=True,
|
||||||
|
help_text="Anything else you'd like the program committee to know when making their selection: your past speaking experience, open source community experience, etc. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>."
|
||||||
|
)
|
||||||
|
submitted = models.DateTimeField(
|
||||||
|
default=datetime.datetime.now,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
speaker = models.ForeignKey("speakers.Speaker", related_name="proposals")
|
||||||
|
additional_speakers = models.ManyToManyField("speakers.Speaker", through="AdditionalSpeaker", blank=True)
|
||||||
|
cancelled = models.BooleanField(default=False)
|
||||||
|
|
||||||
|
def can_edit(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def speaker_email(self):
|
||||||
|
return self.speaker.email
|
||||||
|
|
||||||
|
@property
|
||||||
|
def number(self):
|
||||||
|
return str(self.pk).zfill(3)
|
||||||
|
|
||||||
|
def speakers(self):
|
||||||
|
yield self.speaker
|
||||||
|
for speaker in self.additional_speakers.exclude(additionalspeaker__status=AdditionalSpeaker.SPEAKING_STATUS_DECLINED):
|
||||||
|
yield speaker
|
||||||
|
|
||||||
|
|
||||||
|
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("speakers.Speaker")
|
||||||
|
proposalbase = models.ForeignKey(ProposalBase)
|
||||||
|
status = models.IntegerField(choices=SPEAKING_STATUS, default=SPEAKING_STATUS_PENDING)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = "proposals_proposalbase_additional_speakers"
|
||||||
|
unique_together = ("speaker", "proposalbase")
|
||||||
|
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
uploaded_by = models.ForeignKey(User)
|
||||||
|
created_at = models.DateTimeField(default=datetime.datetime.now)
|
||||||
|
|
||||||
|
file = models.FileField(upload_to=uuid_filename)
|
||||||
|
description = models.CharField(max_length=140)
|
||||||
|
|
||||||
|
def download_url(self):
|
||||||
|
return reverse("proposal_document_download", args=[self.pk, os.path.basename(self.file.name).lower()])
|
0
symposion/proposals/templatetags/__init__.py
Normal file
0
symposion/proposals/templatetags/__init__.py
Normal file
73
symposion/proposals/templatetags/proposal_tags.py
Normal file
73
symposion/proposals/templatetags/proposal_tags.py
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
from symposion.proposals.models import AdditionalSpeaker
|
||||||
|
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
class AssociatedProposalsNode(template.Node):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_token(cls, parser, token):
|
||||||
|
bits = token.split_contents()
|
||||||
|
if len(bits) == 3 and bits[1] == "as":
|
||||||
|
return cls(bits[2])
|
||||||
|
else:
|
||||||
|
raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
|
||||||
|
|
||||||
|
def __init__(self, context_var):
|
||||||
|
self.context_var = context_var
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
request = context["request"]
|
||||||
|
if request.user.speaker_profile:
|
||||||
|
pending = AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED
|
||||||
|
speaker = request.user.speaker_profile
|
||||||
|
queryset = AdditionalSpeaker.objects.filter(speaker=speaker, status=pending)
|
||||||
|
context[self.context_var] = [item.proposalbase for item in queryset]
|
||||||
|
else:
|
||||||
|
context[self.context_var] = None
|
||||||
|
return u""
|
||||||
|
|
||||||
|
|
||||||
|
class PendingProposalsNode(template.Node):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def handle_token(cls, parser, token):
|
||||||
|
bits = token.split_contents()
|
||||||
|
if len(bits) == 3 and bits[1] == "as":
|
||||||
|
return cls(bits[2])
|
||||||
|
else:
|
||||||
|
raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
|
||||||
|
|
||||||
|
def __init__(self, context_var):
|
||||||
|
self.context_var = context_var
|
||||||
|
|
||||||
|
def render(self, context):
|
||||||
|
request = context["request"]
|
||||||
|
if request.user.speaker_profile:
|
||||||
|
pending = AdditionalSpeaker.SPEAKING_STATUS_PENDING
|
||||||
|
speaker = request.user.speaker_profile
|
||||||
|
queryset = AdditionalSpeaker.objects.filter(speaker=speaker, status=pending)
|
||||||
|
context[self.context_var] = [item.proposalbase for item in queryset]
|
||||||
|
else:
|
||||||
|
context[self.context_var] = None
|
||||||
|
return u""
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def pending_proposals(parser, token):
|
||||||
|
"""
|
||||||
|
{% pending_proposals as pending_proposals %}
|
||||||
|
"""
|
||||||
|
return PendingProposalsNode.handle_token(parser, token)
|
||||||
|
|
||||||
|
|
||||||
|
@register.tag
|
||||||
|
def associated_proposals(parser, token):
|
||||||
|
"""
|
||||||
|
{% associated_proposals as associated_proposals %}
|
||||||
|
"""
|
||||||
|
return AssociatedProposalsNode.handle_token(parser, token)
|
||||||
|
|
18
symposion/proposals/urls.py
Normal file
18
symposion/proposals/urls.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
from django.conf.urls.defaults import *
|
||||||
|
|
||||||
|
|
||||||
|
urlpatterns = patterns("symposion.proposals.views",
|
||||||
|
url(r"^submit/$", "proposal_submit", name="proposal_submit"),
|
||||||
|
url(r"^submit/(\w+)/$", "proposal_submit_kind", name="proposal_submit_kind"),
|
||||||
|
url(r"^(\d+)/$", "proposal_detail", name="proposal_detail"),
|
||||||
|
url(r"^(\d+)/edit/$", "proposal_edit", name="proposal_edit"),
|
||||||
|
url(r"^(\d+)/speakers/$", "proposal_speaker_manage", name="proposal_speaker_manage"),
|
||||||
|
url(r"^(\d+)/cancel/$", "proposal_cancel", name="proposal_cancel"),
|
||||||
|
url(r"^(\d+)/leave/$", "proposal_leave", name="proposal_leave"),
|
||||||
|
url(r"^(\d+)/join/$", "proposal_pending_join", name="proposal_pending_join"),
|
||||||
|
url(r"^(\d+)/decline/$", "proposal_pending_decline", name="proposal_pending_decline"),
|
||||||
|
|
||||||
|
url(r"^(\d+)/document/create/$", "document_create", name="proposal_document_create"),
|
||||||
|
url(r"^document/(\d+)/delete/$", "document_delete", name="proposal_document_delete"),
|
||||||
|
url(r"^document/(\d+)/([^/]+)$", "document_download", name="proposal_document_download"),
|
||||||
|
)
|
324
symposion/proposals/views.py
Normal file
324
symposion/proposals/views.py
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
import random
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.http import Http404, HttpResponse, HttpResponseForbidden
|
||||||
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
|
from django.utils.hashcompat import sha_constructor
|
||||||
|
from django.views import static
|
||||||
|
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
|
from account.models import EmailAddress
|
||||||
|
from symposion.proposals.models import ProposalBase, ProposalSection, ProposalKind
|
||||||
|
from symposion.proposals.models import SupportingDocument, AdditionalSpeaker
|
||||||
|
from symposion.speakers.models import Speaker
|
||||||
|
from symposion.utils.mail import send_email
|
||||||
|
|
||||||
|
from symposion.proposals.forms import AddSpeakerForm, SupportingDocumentCreateForm
|
||||||
|
|
||||||
|
|
||||||
|
def get_form(name):
|
||||||
|
dot = name.rindex('.')
|
||||||
|
mod_name, form_name = name[:dot], name[dot + 1:]
|
||||||
|
__import__(mod_name)
|
||||||
|
return getattr(sys.modules[mod_name], form_name)
|
||||||
|
|
||||||
|
|
||||||
|
def proposal_submit(request):
|
||||||
|
if not request.user.is_authenticated():
|
||||||
|
return redirect("home") # @@@ unauth'd speaker info page?
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
request.user.speaker_profile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
kinds = []
|
||||||
|
for proposal_section in ProposalSection.available():
|
||||||
|
for kind in proposal_section.section.proposal_kinds.all():
|
||||||
|
kinds.append(kind)
|
||||||
|
|
||||||
|
return render(request, "proposals/proposal_submit.html", {
|
||||||
|
"kinds": kinds,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
def proposal_submit_kind(request, kind_slug):
|
||||||
|
|
||||||
|
kind = get_object_or_404(ProposalKind, slug=kind_slug)
|
||||||
|
|
||||||
|
if not request.user.is_authenticated():
|
||||||
|
return redirect("home") # @@@ unauth'd speaker info page?
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
speaker_profile = request.user.speaker_profile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
form_class = get_form(settings.PROPOSAL_FORMS[kind_slug])
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = form_class(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
proposal = form.save(commit=False)
|
||||||
|
proposal.kind = kind
|
||||||
|
proposal.speaker = speaker_profile
|
||||||
|
proposal.save()
|
||||||
|
form.save_m2m()
|
||||||
|
messages.success(request, "Proposal submitted.")
|
||||||
|
if "add-speakers" in request.POST:
|
||||||
|
return redirect("proposal_speaker_manage", proposal.pk)
|
||||||
|
return redirect("dashboard")
|
||||||
|
else:
|
||||||
|
form = form_class()
|
||||||
|
|
||||||
|
return render(request, "proposals/proposal_submit_kind.html", {
|
||||||
|
"kind": kind,
|
||||||
|
"form": form,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def proposal_speaker_manage(request, pk):
|
||||||
|
queryset = ProposalBase.objects.select_related("speaker")
|
||||||
|
proposal = get_object_or_404(queryset, pk=pk)
|
||||||
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
||||||
|
|
||||||
|
if proposal.speaker != request.user.speaker_profile:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
add_speaker_form = AddSpeakerForm(request.POST, proposal=proposal)
|
||||||
|
if add_speaker_form.is_valid():
|
||||||
|
message_ctx = {
|
||||||
|
"proposal": proposal,
|
||||||
|
}
|
||||||
|
|
||||||
|
def create_speaker_token(email_address):
|
||||||
|
# create token and look for an existing speaker to prevent
|
||||||
|
# duplicate tokens and confusing the pending speaker
|
||||||
|
try:
|
||||||
|
pending = Speaker.objects.get(
|
||||||
|
Q(user=None, invite_email=email_address)
|
||||||
|
)
|
||||||
|
except Speaker.DoesNotExist:
|
||||||
|
salt = sha_constructor(str(random.random())).hexdigest()[:5]
|
||||||
|
token = sha_constructor(salt + email_address).hexdigest()
|
||||||
|
pending = Speaker.objects.create(
|
||||||
|
invite_email=email_address,
|
||||||
|
invite_token=token,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
token = pending.invite_token
|
||||||
|
return pending, token
|
||||||
|
email_address = add_speaker_form.cleaned_data["email"]
|
||||||
|
# check if email is on the site now
|
||||||
|
users = EmailAddress.objects.get_users_for(email_address)
|
||||||
|
if users:
|
||||||
|
# should only be one since we enforce unique email
|
||||||
|
user = users[0]
|
||||||
|
message_ctx["user"] = user
|
||||||
|
# look for speaker profile
|
||||||
|
try:
|
||||||
|
speaker = user.speaker_profile
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
speaker, token = create_speaker_token(email_address)
|
||||||
|
message_ctx["token"] = token
|
||||||
|
# fire off email to user to create profile
|
||||||
|
send_email(
|
||||||
|
[email_address], "speaker_no_profile",
|
||||||
|
context = message_ctx
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# fire off email to user letting them they are loved.
|
||||||
|
send_email(
|
||||||
|
[email_address], "speaker_addition",
|
||||||
|
context = message_ctx
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
speaker, token = create_speaker_token(email_address)
|
||||||
|
message_ctx["token"] = token
|
||||||
|
# fire off email letting user know about site and to create
|
||||||
|
# account and speaker profile
|
||||||
|
send_email(
|
||||||
|
[email_address], "speaker_invite",
|
||||||
|
context = message_ctx
|
||||||
|
)
|
||||||
|
invitation, created = AdditionalSpeaker.objects.get_or_create(proposalbase=proposal.proposalbase_ptr, speaker=speaker)
|
||||||
|
messages.success(request, "Speaker invited to proposal.")
|
||||||
|
return redirect("proposal_speaker_manage", proposal.pk)
|
||||||
|
else:
|
||||||
|
add_speaker_form = AddSpeakerForm(proposal=proposal)
|
||||||
|
ctx = {
|
||||||
|
"proposal": proposal,
|
||||||
|
"speakers": proposal.speakers(),
|
||||||
|
"add_speaker_form": add_speaker_form,
|
||||||
|
}
|
||||||
|
return render(request, "proposals/proposal_speaker_manage.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def proposal_edit(request, pk):
|
||||||
|
queryset = ProposalBase.objects.select_related("speaker")
|
||||||
|
proposal = get_object_or_404(queryset, pk=pk)
|
||||||
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
||||||
|
|
||||||
|
if request.user != proposal.speaker.user:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
if not proposal.can_edit():
|
||||||
|
ctx = {
|
||||||
|
"title": "Proposal editing closed",
|
||||||
|
"body": "Proposal editing is closed for this session type."
|
||||||
|
}
|
||||||
|
return render(request, "proposals/proposal_error.html", ctx)
|
||||||
|
|
||||||
|
form_class = get_form(settings.PROPOSAL_FORMS[proposal.kind.slug])
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = form_class(request.POST, instance=proposal)
|
||||||
|
if form.is_valid():
|
||||||
|
form.save()
|
||||||
|
messages.success(request, "Proposal updated.")
|
||||||
|
return redirect("proposal_detail", proposal.pk)
|
||||||
|
else:
|
||||||
|
form = form_class(instance=proposal)
|
||||||
|
|
||||||
|
return render(request, "proposals/proposal_edit.html", {
|
||||||
|
"proposal": proposal,
|
||||||
|
"form": form,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def proposal_detail(request, pk):
|
||||||
|
queryset = ProposalBase.objects.select_related("speaker", "speaker__user")
|
||||||
|
proposal = get_object_or_404(queryset, pk=pk)
|
||||||
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
||||||
|
|
||||||
|
if request.user not in [p.user for p in proposal.speakers()]:
|
||||||
|
raise Http404()
|
||||||
|
|
||||||
|
return render(request, "proposals/proposal_detail.html", {
|
||||||
|
"proposal": proposal,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def proposal_cancel(request, pk):
|
||||||
|
queryset = ProposalBase.objects.select_related("speaker")
|
||||||
|
proposal = get_object_or_404(queryset, pk=pk)
|
||||||
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
||||||
|
|
||||||
|
if proposal.speaker.user != request.user:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
proposal.cancelled = True
|
||||||
|
proposal.save()
|
||||||
|
# @@@ fire off email to submitter and other speakers
|
||||||
|
messages.success(request, "%s has been cancelled" % proposal.title)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
return render(request, "proposals/proposal_cancel.html", {
|
||||||
|
"proposal": proposal,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def proposal_leave(request, pk):
|
||||||
|
queryset = ProposalBase.objects.select_related("speaker")
|
||||||
|
proposal = get_object_or_404(queryset, pk=pk)
|
||||||
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
||||||
|
|
||||||
|
try:
|
||||||
|
speaker = proposal.additional_speakers.get(user=request.user)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
return HttpResponseForbidden()
|
||||||
|
if request.method == "POST":
|
||||||
|
proposal.additional_speakers.remove(speaker)
|
||||||
|
# @@@ fire off email to submitter and other speakers
|
||||||
|
messages.success(request, "You are no longer speaking on %s" % proposal.title)
|
||||||
|
return redirect("speaker_dashboard")
|
||||||
|
ctx = {
|
||||||
|
"proposal": proposal,
|
||||||
|
}
|
||||||
|
return render(request, "proposals/proposal_leave.html", ctx)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def proposal_pending_join(request, pk):
|
||||||
|
proposal = get_object_or_404(ProposalBase, pk=pk)
|
||||||
|
speaking = get_object_or_404(AdditionalSpeaker, speaker=request.user.speaker_profile, proposalbase=proposal)
|
||||||
|
if speaking.status == AdditionalSpeaker.SPEAKING_STATUS_PENDING:
|
||||||
|
speaking.status = AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED
|
||||||
|
speaking.save()
|
||||||
|
messages.success(request, "You have accepted the invitation to join %s" % proposal.title)
|
||||||
|
return redirect("dashboard")
|
||||||
|
else:
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def proposal_pending_decline(request, pk):
|
||||||
|
proposal = get_object_or_404(ProposalBase, pk=pk)
|
||||||
|
speaking = get_object_or_404(AdditionalSpeaker, speaker=request.user.speaker_profile, proposalbase=proposal)
|
||||||
|
if speaking.status == AdditionalSpeaker.SPEAKING_STATUS_PENDING:
|
||||||
|
speaking.status = AdditionalSpeaker.SPEAKING_STATUS_DECLINED
|
||||||
|
speaking.save()
|
||||||
|
messages.success(request, "You have declined to speak on %s" % proposal.title)
|
||||||
|
return redirect("dashboard")
|
||||||
|
else:
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def document_create(request, proposal_pk):
|
||||||
|
queryset = ProposalBase.objects.select_related("speaker")
|
||||||
|
proposal = get_object_or_404(queryset, pk=proposal_pk)
|
||||||
|
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
form = SupportingDocumentCreateForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
document = form.save(commit=False)
|
||||||
|
document.proposal = proposal
|
||||||
|
document.uploaded_by = request.user
|
||||||
|
document.save()
|
||||||
|
return redirect("proposal_detail", proposal.pk)
|
||||||
|
else:
|
||||||
|
form = SupportingDocumentCreateForm()
|
||||||
|
|
||||||
|
return render(request, "proposals/document_create.html", {
|
||||||
|
"proposal": proposal,
|
||||||
|
"form": form,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def document_download(request, pk, *args):
|
||||||
|
document = get_object_or_404(SupportingDocument, pk=pk)
|
||||||
|
if settings.USE_X_ACCEL_REDIRECT:
|
||||||
|
response = HttpResponse()
|
||||||
|
response["X-Accel-Redirect"] = document.file.url
|
||||||
|
# delete content-type to allow Gondor to determine the filetype and
|
||||||
|
# we definitely don't want Django's crappy default :-)
|
||||||
|
del response["content-type"]
|
||||||
|
else:
|
||||||
|
response = static.serve(request, document.file.name, document_root=settings.MEDIA_ROOT)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def document_delete(request, pk):
|
||||||
|
document = get_object_or_404(SupportingDocument, pk=pk, uploaded_by=request.user)
|
||||||
|
proposal_pk = document.proposal.pk
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
document.delete()
|
||||||
|
|
||||||
|
return redirect("proposal_detail", proposal_pk)
|
Loading…
Reference in a new issue