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…
	
	Add table
		
		Reference in a new issue
	
	 Luke Hatcher
						Luke Hatcher