Changeset - 6d5c24e6354f
[Not reviewed]
Merge
! ! !
Patrick Altman - 10 years ago 2014-07-30 21:16:03
paltman@gmail.com
Merge pull request #56 from chromano/master

Fix flake8 warnings
66 files changed with 611 insertions and 604 deletions:
0 comments (0 inline, 0 general)
symposion/boxes/authorization.py
Show inline comments
...
 
@@ -10,11 +10,11 @@ def default_can_edit(request, *args, **kwargs):
 
    """
 
    return request.user.is_staff or request.user.is_superuser
 

	
 

	
 
def load_can_edit():
 
    import_path = getattr(settings, "BOXES_CAN_EDIT_CALLABLE", None)
 
    
 

 
    if import_path is None:
 
        return default_can_edit
 
    
 

 
    return load_path_attr(import_path)
symposion/boxes/forms.py
Show inline comments
 
from django import forms
 

	
 
from symposion.boxes.models import Box
 

	
 

	
 
class BoxForm(forms.ModelForm):
 
    
 

 
    class Meta:
 
        model = Box
 
        fields = ["content"]
symposion/boxes/models.py
Show inline comments
 
import datetime
 

	
 
from django.db import models
 

	
 
from django.contrib.auth.models import User
 

	
 
import reversion
 

	
 
from markitup.fields import MarkupField
 

	
 

	
 
class Box(models.Model):
 
    
 

 
    label = models.CharField(max_length=100, db_index=True)
 
    content = MarkupField(blank=True)
 
    
 

 
    created_by = models.ForeignKey(User, related_name="boxes")
 
    last_updated_by = models.ForeignKey(User, related_name="updated_boxes")
 
    
 

 
    def __unicode__(self):
 
        return self.label
 
    
 

 
    class Meta:
 
        verbose_name_plural = "boxes"
 

	
 

	
 
reversion.register(Box)
symposion/boxes/templatetags/boxes_tags.py
Show inline comments
 
from django import template
 
from django.core.exceptions import ImproperlyConfigured
 
from django.core.urlresolvers import reverse
 
from django.utils.safestring import mark_safe
 
from django.utils.encoding import smart_str
 
from django.utils.translation import ugettext_lazy as _
 
from django.template.defaulttags import kwarg_re
 

	
 
from symposion.boxes.models import Box
 
from symposion.boxes.forms import BoxForm
 
from symposion.boxes.authorization import load_can_edit
 

	
 

	
 
register = template.Library()
 

	
 

	
 
@register.inclusion_tag("boxes/box.html", takes_context=True)
 
def box(context, label, show_edit=True, *args, **kwargs):
 
    
 

 
    request = context["request"]
 
    can_edit = load_can_edit()(request, *args, **kwargs)
 
    
 

 
    try:
 
        box = Box.objects.get(label=label)
 
    except Box.DoesNotExist:
 
        box = None
 
    
 

 
    if can_edit and show_edit:
 
        form = BoxForm(instance=box, prefix=label)
 
        form_action = reverse("box_edit", args=[label])
 
    else:
 
        form = None
 
        form_action = None
 
    
 

 
    return {
 
        "request": request,
 
        "label": label,
 
        "box": box,
 
        "form": form,
 
        "form_action": form_action,
symposion/boxes/urls.py
Show inline comments
 
# flake8: noqa
 
from django.conf.urls.defaults import url, patterns
 

	
 

	
 
urlpatterns = patterns("symposion.boxes.views",
 
    url(r"^([-\w]+)/edit/$", "box_edit", name="box_edit"),
 
)
...
 
\ No newline at end of file
 
)
symposion/boxes/views.py
Show inline comments
...
 
@@ -4,36 +4,37 @@ from django.views.decorators.http import require_POST
 

	
 
from symposion.boxes.authorization import load_can_edit
 
from symposion.boxes.forms import BoxForm
 
from symposion.boxes.models import Box
 

	
 

	
 
# @@@ problem with this is that the box_edit.html and box_create.html won't have domain objects in context
 
# @@@ problem with this is that the box_edit.html and box_create.html won't have domain objects in
 
# context
 
def get_auth_vars(request):
 
    auth_vars = {}
 
    if request.method == "POST":
 
        keys = [k for k in request.POST.keys() if k.startswith("boxes_auth_")]
 
        for key in keys:
 
            auth_vars[key.replace("boxes_auth_", "")] = request.POST.get(key)
 
        auth_vars["user"] = request.user
 
    return auth_vars
 

	
 

	
 
@require_POST
 
def box_edit(request, label):
 
    
 

 
    if not load_can_edit()(request, **get_auth_vars(request)):
 
        return HttpResponseForbidden()
 
    
 

 
    next = request.GET.get("next")
 
    
 

 
    try:
 
        box = Box.objects.get(label=label)
 
    except Box.DoesNotExist:
 
        box = None
 
    
 

 
    form = BoxForm(request.POST, instance=box, prefix=label)
 

	
 
    if form.is_valid():
 
        if box is None:
 
            box = form.save(commit=False)
 
            box.label = label
symposion/cms/admin.py
Show inline comments
 
from django.contrib import admin
 

	
 
import reversion
 

	
 
from .models import Page
 

	
 

	
 
class PageAdmin(reversion.VersionAdmin):
 

	
 
    pass
 

	
 

	
 
admin.site.register(Page, PageAdmin)
symposion/cms/forms.py
Show inline comments
...
 
@@ -3,19 +3,19 @@ from django import forms
 
from markitup.widgets import MarkItUpWidget
 

	
 
from .models import Page
 

	
 

	
 
class PageForm(forms.ModelForm):
 
    
 

 
    class Meta:
 
        model = Page
 
        fields = ["title", "body", "path"]
 
        widgets = {
 
            "body": MarkItUpWidget(),
 
            "path": forms.HiddenInput(),
 
        }
 

	
 

	
 
class FileUploadForm(forms.Form):
 
    
 

 
    file = forms.FileField()
symposion/cms/managers.py
Show inline comments
 
from datetime import datetime
 

	
 
from django.db import models
 

	
 

	
 
class PublishedPageManager(models.Manager):
 
    
 

 
    def get_query_set(self):
 
        qs = super(PublishedPageManager, self).get_query_set()
 
        return qs.filter(publish_date__lte=datetime.now())
symposion/cms/models.py
Show inline comments
...
 
@@ -15,58 +15,59 @@ from taggit.managers import TaggableManager
 
import reversion
 

	
 
from .managers import PublishedPageManager
 

	
 

	
 
class Page(models.Model):
 
    
 

 
    STATUS_CHOICES = (
 
        (1, _("Draft")),
 
        (2, _("Public")),
 
    )
 
    
 

 
    title = models.CharField(max_length=100)
 
    path = models.CharField(max_length=100, unique=True)
 
    body = MarkupField()
 
    status = models.IntegerField(choices=STATUS_CHOICES, default=2)
 
    publish_date = models.DateTimeField(default=datetime.datetime.now)
 
    created = models.DateTimeField(editable=False, default=datetime.datetime.now)
 
    updated = models.DateTimeField(editable=False, default=datetime.datetime.now)
 
    tags = TaggableManager(blank=True)
 
    
 

 
    published = PublishedPageManager()
 
    
 

 
    def __unicode__(self):
 
        return self.title
 
    
 

 
    @models.permalink
 
    def get_absolute_url(self):
 
        return ("cms_page", [self.path])
 
    
 

 
    @property
 
    def is_community(self):
 
        return self.path.lower().startswith("community/")
 
    
 

 
    def save(self, *args, **kwargs):
 
        self.updated = datetime.datetime.now()
 
        super(Page, self).save(*args, **kwargs)
 
    
 

 
    def clean_fields(self, exclude=None):
 
        super(Page, self).clean_fields(exclude)
 
        if not re.match(settings.SYMPOSION_PAGE_REGEX, self.path):
 
            raise ValidationError({"path": [_("Path can only contain letters, numbers and hyphens and end with /")]})
 
            raise ValidationError(
 
                {"path": [_("Path can only contain letters, numbers and hyphens and end with /")]})
 

	
 

	
 
reversion.register(Page)
 

	
 

	
 
def generate_filename(instance, filename):
 
    return filename
 

	
 

	
 
class File(models.Model):
 
    
 

 
    file = models.FileField(upload_to=generate_filename)
 
    created = models.DateTimeField(default=datetime.datetime.now)
 
    
 

 
    def download_url(self):
 
        return reverse("file_download", args=[self.pk, os.path.basename(self.file.name).lower()])
symposion/cms/urls.py
Show inline comments
 
# flake8: noqa
 
from django.conf.urls.defaults import url, patterns
 

	
 
PAGE_RE = r"(([\w-]{1,})(/[\w-]{1,})*)/"
 

	
 
urlpatterns = patterns("symposion.cms.views",
 
    url(r"^files/$", "file_index", name="file_index"),
symposion/cms/views.py
Show inline comments
...
 
@@ -20,111 +20,111 @@ def can_upload(user):
 
    if user.is_staff or user.is_superuser:
 
        return True
 
    return False
 

	
 

	
 
def page(request, path):
 
    
 

 
    try:
 
        page = Page.published.get(path=path)
 
    except Page.DoesNotExist:
 
        page = None
 
    
 

 
    editable = can_edit(page, request.user)
 
    
 

 
    if page is None:
 
        if editable:
 
            return redirect("cms_page_edit", path=path)
 
        else:
 
            raise Http404
 
    
 

 
    return render(request, "cms/page_detail.html", {
 
        "page": page,
 
        "editable": editable,
 
    })
 

	
 

	
 
@login_required
 
def page_edit(request, path):
 
    
 

 
    try:
 
        page = Page.published.get(path=path)
 
    except Page.DoesNotExist:
 
        page = None
 
    
 

 
    if not can_edit(page, request.user):
 
        raise Http404
 
    
 

 
    if request.method == "POST":
 
        form = PageForm(request.POST, instance=page)
 
        if form.is_valid():
 
            page = form.save(commit=False)
 
            page.path = path
 
            page.save()
 
            return redirect(page)
 
        else:
 
            print form.errors
 
    else:
 
        form = PageForm(instance=page, initial={"path": path})
 
    
 

 
    return render(request, "cms/page_edit.html", {
 
        "path": path,
 
        "form": form
 
    })
 

	
 

	
 
def file_index(request):
 
    if not can_upload(request.user):
 
        raise Http404
 
    
 

 
    ctx = {
 
        "files": File.objects.all(),
 
    }
 
    return render(request, "cms/file_index.html", ctx)
 

	
 

	
 
def file_create(request):
 
    if not can_upload(request.user):
 
        raise Http404
 
    
 

 
    if request.method == "POST":
 
        form = FileUploadForm(request.POST, request.FILES)
 
        if form.is_valid():
 
            with transaction.commit_on_success():
 
                kwargs = {
 
                    "file": form.cleaned_data["file"],
 
                }
 
                File.objects.create(**kwargs)
 
            return redirect("file_index")
 
    else:
 
        form = FileUploadForm()
 
    
 

 
    ctx = {
 
        "form": form,
 
    }
 
    return render(request, "cms/file_create.html", ctx)
 

	
 

	
 
def file_download(request, pk, *args):
 
    file = get_object_or_404(File, pk=pk)
 
    
 

 
    if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
 
        response = HttpResponse()
 
        response["X-Accel-Redirect"] = file.file.url
 
        # delete content-type to allow Gondor to determine the filetype and
 
        # we definitely don't want Django's default :-)
 
        del response["content-type"]
 
    else:
 
        response = static.serve(request, file.file.name, document_root=settings.MEDIA_ROOT)
 
    
 

 
    return response
 

	
 

	
 
def file_delete(request, pk):
 
    if not can_upload(request.user):
 
        raise Http404
 
    
 

 
    file = get_object_or_404(File, pk=pk)
 
    if request.method == "POST":
 
        file.delete()
 
        # @@@ message
 
    return redirect("file_index")
symposion/conf.py
Show inline comments
 
from django.conf import settings
 

	
 
from appconf import AppConf
 

	
 

	
 
class SymposionAppConf(AppConf):
 
    
 

 
    VOTE_THRESHOLD = 3
symposion/conference/admin.py
Show inline comments
...
 
@@ -3,9 +3,9 @@ from django.contrib import admin
 
from symposion.conference.models import Conference, Section
 

	
 

	
 
admin.site.register(Conference, list_display=("title", "start_date", "end_date"))
 
admin.site.register(
 
    Section,
 
    prepopulated_fields = {"slug": ("name",)},
 
    list_display = ("name", "conference", "start_date", "end_date")
 
    prepopulated_fields={"slug": ("name",)},
 
    list_display=("name", "conference", "start_date", "end_date")
 
)
symposion/conference/models.py
Show inline comments
...
 
@@ -8,62 +8,62 @@ CONFERENCE_CACHE = {}
 

	
 

	
 
class Conference(models.Model):
 
    """
 
    the full conference for a specific year, e.g. US PyCon 2012.
 
    """
 
    
 

 
    title = models.CharField(_("title"), max_length=100)
 
    
 

 
    # when the conference runs
 
    start_date = models.DateField(_("start date"), null=True, blank=True)
 
    end_date = models.DateField(_("end date"), null=True, blank=True)
 
    
 

 
    # timezone the conference is in
 
    timezone = TimeZoneField(_("timezone"), blank=True)
 
    
 

 
    def __unicode__(self):
 
        return self.title
 
    
 

 
    def save(self, *args, **kwargs):
 
        super(Conference, self).save(*args, **kwargs)
 
        if self.id in CONFERENCE_CACHE:
 
            del CONFERENCE_CACHE[self.id]
 
    
 

 
    def delete(self):
 
        pk = self.pk
 
        super(Conference, self).delete()
 
        try:
 
            del CONFERENCE_CACHE[pk]
 
        except KeyError:
 
            pass
 
    
 

 
    class Meta(object):
 
        verbose_name = _("conference")
 
        verbose_name_plural = _("conferences")
 

	
 

	
 
class Section(models.Model):
 
    """
 
    a section of the conference such as "Tutorials", "Workshops",
 
    "Talks", "Expo", "Sprints", that may have its own review and
 
    scheduling process.
 
    """
 
    
 

 
    conference = models.ForeignKey(Conference, verbose_name=_("conference"))
 
    
 

 
    name = models.CharField(_("name"), max_length=100)
 
    slug = models.SlugField()
 

	
 
    # when the section runs
 
    start_date = models.DateField(_("start date"), null=True, blank=True)
 
    end_date = models.DateField(_("end date"), null=True, blank=True)
 
    
 

 
    def __unicode__(self):
 
        return "%s %s" % (self.conference, self.name)
 
    
 

 
    class Meta(object):
 
        verbose_name = _("section")
 
        verbose_name_plural = _("sections")
 
        ordering = ["start_date"]
 

	
 

	
symposion/conference/urls.py
Show inline comments
 
from django.conf.urls.defaults import *
 
# flake8: noqa
 
from django.conf.urls.defaults import patterns, url
 

	
 

	
 
urlpatterns = patterns("symposion.conference.views",
 
    url(r"^users/$", "user_list", name="user_list"),
 
)
symposion/conference/views.py
Show inline comments
...
 
@@ -4,13 +4,13 @@ from django.shortcuts import render
 
from django.contrib.auth.decorators import login_required
 
from django.contrib.auth.models import User
 

	
 

	
 
@login_required
 
def user_list(request):
 
    
 

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

 
    return render(request, "conference/user_list.html", {
 
        "users": User.objects.all(),
 
    })
symposion/forms.py
Show inline comments
 
from django import forms
 

	
 
import account.forms
 

	
 

	
 
class SignupForm(account.forms.SignupForm):
 
    
 

 
    first_name = forms.CharField()
 
    last_name = forms.CharField()
 
    email_confirm = forms.EmailField(label="Confirm Email")
 

	
 
    def __init__(self, *args, **kwargs):
 
        super(SignupForm, self).__init__(*args, **kwargs)
...
 
@@ -17,14 +17,15 @@ class SignupForm(account.forms.SignupForm):
 
            "email_confirm",
 
            "first_name",
 
            "last_name",
 
            "password",
 
            "password_confirm"
 
        ]
 
    
 

 
    def clean_email_confirm(self):
 
        email = self.cleaned_data.get("email")
 
        email_confirm = self.cleaned_data["email_confirm"]
 
        if email:
 
            if email != email_confirm:
 
                raise forms.ValidationError("Email address must match previously typed email address")
 
                raise forms.ValidationError(
 
                    "Email address must match previously typed email address")
 
        return email_confirm
symposion/markdown_parser.py
Show inline comments
 
import html5lib
 
from html5lib import html5parser, sanitizer
 

	
 
import markdown
 

	
 

	
 
def parse(text):
 
    
 

 
    # First run through the Markdown parser
 
    text = markdown.markdown(text, extensions=["extra"], safe_mode=False)
 
    
 

 
    # Sanitize using html5lib
 
    bits = []
 
    parser = html5parser.HTMLParser(tokenizer=sanitizer.HTMLSanitizer)
 
    for token in parser.parseFragment(text).childNodes:
 
        bits.append(token.toxml())
 
    return "".join(bits)
symposion/proposals/actions.py
Show inline comments
 
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):
 
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):
...
 
@@ -21,15 +20,17 @@ def export_as_csv_action(
 
            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(".", "_")
 
        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])
 
            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
symposion/proposals/forms.py
Show inline comments
...
 
@@ -6,21 +6,21 @@ from symposion.proposals.models import SupportingDocument
 

	
 

	
 
# @@@ 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()
...
 
@@ -29,13 +29,13 @@ class AddSpeakerForm(forms.Form):
 
                "This email address has already been invited to your talk proposal"
 
            )
 
        return value
 

	
 

	
 
class SupportingDocumentCreateForm(forms.ModelForm):
 
    
 

 
    class Meta:
 
        model = SupportingDocument
 
        fields = [
 
            "file",
 
            "description",
 
        ]
symposion/proposals/managers.py
Show inline comments
 
deleted file
symposion/proposals/models.py
Show inline comments
...
 
@@ -18,158 +18,168 @@ 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 is_available(self):
 
        if self.closed:
 
            return False
 
        now = datetime.datetime.now()
 
        if self.start and self.start > now:
 
            return False
 
        if self.end and self.end < now:
 
            return False
 
        return True
 
    
 

 
    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(
 
        _("Brief Description"),
 
        max_length=400,  # @@@ need to enforce 400 in UI
 
        help_text="If your proposal is accepted this will be made public and printed in the program. Should be one paragraph, maximum 400 characters."
 
        help_text=_("If your proposal is accepted this will be made public and printed in the "
 
                    "program. Should be one paragraph, maximum 400 characters.")
 
    )
 
    abstract = MarkupField(
 
        _("Detailed Abstract"),
 
        help_text=_("Detailed outline. Will be made public if your proposal is accepted. Edit using <a href='http://daringfireball.net/projects/markdown/basics' target='_blank'>Markdown</a>.")
 
        help_text=_("Detailed outline. Will be made public if your proposal is accepted. Edit "
 
                    "using <a href='http://daringfireball.net/projects/markdown/basics' "
 
                    "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 experience, etc. This is not made public. Edit using <a href='http://daringfireball.net/projects/markdown/basics' target='_blank'>Markdown</a>.")
 
        help_text=_("Anything else you'd like the program committee to know when making their "
 
                    "selection: your past experience, etc. This is not made public. Edit using "
 
                    "<a href='http://daringfireball.net/projects/markdown/basics' "
 
                    "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)
 
    additional_speakers = models.ManyToManyField("speakers.Speaker", through="AdditionalSpeaker",
 
                                                 blank=True)
 
    cancelled = models.BooleanField(default=False)
 
    
 

 
    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)
 
    
 

 
    def speakers(self):
 
        yield self.speaker
 
        for speaker in self.additional_speakers.exclude(additionalspeaker__status=AdditionalSpeaker.SPEAKING_STATUS_DECLINED):
 
        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,
 
            "kind": self.kind.name,
 
        }
 

	
 

	
 
reversion.register(ProposalBase)
 

	
 

	
 
class AdditionalSpeaker(models.Model):
 
    
 

 
    SPEAKING_STATUS_PENDING = 1
 
    SPEAKING_STATUS_ACCEPTED = 2
 
    SPEAKING_STATUS_DECLINED = 3
 
    
 

 
    SPEAKING_STATUS = [
 
        (SPEAKING_STATUS_PENDING, _("Pending")),
 
        (SPEAKING_STATUS_ACCEPTED, _("Accepted")),
 
        (SPEAKING_STATUS_DECLINED, _("Declined")),
 
    ]
 
    
 

 
    speaker = models.ForeignKey("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()])
 
        return reverse("proposal_document_download",
 
                       args=[self.pk, os.path.basename(self.file.name).lower()])
symposion/proposals/templatetags/proposal_tags.py
Show inline comments
...
 
@@ -4,24 +4,24 @@ 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)
...
 
@@ -29,24 +29,24 @@ class AssociatedProposalsNode(template.Node):
 
        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)
...
 
@@ -67,7 +67,6 @@ def pending_proposals(parser, token):
 
@register.tag
 
def associated_proposals(parser, token):
 
    """
 
    {% associated_proposals as associated_proposals %}
 
    """
 
    return AssociatedProposalsNode.handle_token(parser, token)
 

	
symposion/proposals/urls.py
Show inline comments
 
# flake8: noqa
 
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"),
...
 
@@ -8,11 +9,11 @@ urlpatterns = patterns("symposion.proposals.views",
 
    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"),
 
)
symposion/proposals/views.py
Show inline comments
...
 
@@ -34,40 +34,40 @@ def proposal_submit(request):
 
        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")
 
    
 

 
    if not kind.section.proposalsection.is_available():
 
        return redirect("proposal_submit")
 
    
 

 
    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
...
 
@@ -76,35 +76,35 @@ def proposal_submit_kind(request, kind_slug):
 
            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)
...
 
@@ -132,30 +132,31 @@ def proposal_speaker_manage(request, pk):
 
                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
 
                        context=message_ctx
 
                    )
 
                else:
 
                    # fire off email to user letting them they are loved.
 
                    send_email(
 
                        [email_address], "speaker_addition",
 
                        context = message_ctx
 
                        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
 
                    context=message_ctx
 
                )
 
            invitation, created = AdditionalSpeaker.objects.get_or_create(proposalbase=proposal.proposalbase_ptr, speaker=speaker)
 
            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,
...
 
@@ -170,20 +171,20 @@ 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()
...
 
@@ -203,88 +204,88 @@ def proposal_edit(request, pk):
 
                        context=ctx
 
                    )
 
            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()
 
    
 

 
    if "symposion.reviews" in settings.INSTALLED_APPS:
 
        from symposion.reviews.forms import SpeakerCommentForm
 
        message_form = SpeakerCommentForm()
 
        if request.method == "POST":
 
            message_form = SpeakerCommentForm(request.POST)
 
            if message_form.is_valid():
 
                
 

 
                message = message_form.save(commit=False)
 
                message.user = request.user
 
                message.proposal = proposal
 
                message.save()
 
                
 

 
                ProposalMessage = SpeakerCommentForm.Meta.model
 
                reviewers = User.objects.filter(
 
                    id__in=ProposalMessage.objects.filter(
 
                        proposal=proposal
 
                    ).exclude(
 
                        user=request.user
 
                    ).distinct().values_list("user", flat=True)
 
                )
 
                
 

 
                for reviewer in reviewers:
 
                    ctx = {
 
                        "proposal": proposal,
 
                        "message": message,
 
                        "reviewer": True,
 
                    }
 
                    send_email(
 
                        [reviewer.email], "proposal_new_message",
 
                        context=ctx
 
                    )
 
                
 

 
                return redirect(request.path)
 
        else:
 
            message_form = SpeakerCommentForm()
 
    else:
 
        message_form = None
 
    
 

 
    return render(request, "proposals/proposal_detail.html", {
 
        "proposal": proposal,
 
        "message_form": message_form
 
    })
 

	
 

	
 
@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
...
 
@@ -308,26 +309,28 @@ def proposal_leave(request, pk):
 
    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)
 
    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)
 
    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:
...
 
@@ -336,27 +339,27 @@ def proposal_pending_decline(request, pk):
 

	
 
@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 proposal.cancelled:
 
        return HttpResponseForbidden()
 
    
 

 
    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,
 
    })
 

	
 

	
...
 
@@ -375,11 +378,11 @@ def document_download(request, pk, *args):
 

	
 

	
 
@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)
symposion/reviews/context_processors.py
Show inline comments
 
from django.contrib.contenttypes.models import ContentType
 

	
 
from symposion.proposals.models import ProposalSection
 

	
 

	
 
def reviews(request):
 
    sections = []
 
    for section in ProposalSection.objects.all():
symposion/reviews/forms.py
Show inline comments
...
 
@@ -6,34 +6,34 @@ from symposion.reviews.models import Review, Comment, ProposalMessage, VOTES
 

	
 

	
 
class ReviewForm(forms.ModelForm):
 
    class Meta:
 
        model = Review
 
        fields = ["vote", "comment"]
 
        widgets = { "comment": MarkItUpWidget() }
 
    
 
        widgets = {"comment": MarkItUpWidget()}
 

	
 
    def __init__(self, *args, **kwargs):
 
        super(ReviewForm, self).__init__(*args, **kwargs)
 
        self.fields["vote"] = forms.ChoiceField(
 
            widget = forms.RadioSelect(),
 
            choices = VOTES.CHOICES
 
            widget=forms.RadioSelect(),
 
            choices=VOTES.CHOICES
 
        )
 

	
 

	
 
class ReviewCommentForm(forms.ModelForm):
 
    class Meta:
 
        model = Comment
 
        fields = ["text"]
 
        widgets = { "text": MarkItUpWidget() }
 
        widgets = {"text": MarkItUpWidget()}
 

	
 

	
 
class SpeakerCommentForm(forms.ModelForm):
 
    class Meta:
 
        model = ProposalMessage
 
        fields = ["message"]
 
        widgets = { "message": MarkItUpWidget() }
 
        widgets = {"message": MarkItUpWidget()}
 

	
 

	
 
class BulkPresentationForm(forms.Form):
 
    talk_ids = forms.CharField(
 
        max_length=500,
 
        help_text="Provide a comma seperated list of talk ids to accept."
symposion/reviews/management/commands/assign_reviewers.py
Show inline comments
 
import csv
 
import os
 
import random
 

	
 
from django.contrib.auth import models
 
from django.core.management.base import BaseCommand
 

	
 
from symposion.reviews.models import ReviewAssignment
 
from symposion.proposals.models import ProposalBase
 

	
 

	
symposion/reviews/management/commands/calculate_results.py
Show inline comments
 
from django.core.management.base import BaseCommand
 

	
 
from django.contrib.auth.models import Group
 

	
 
from symposion.reviews.models import ProposalResult
 

	
 

	
 
class Command(BaseCommand):
 
    
 

 
    def handle(self, *args, **options):
 
        ProposalResult.full_calculate()
symposion/reviews/management/commands/create_review_permissions.py
Show inline comments
...
 
@@ -4,20 +4,20 @@ from django.contrib.auth.models import Permission
 
from django.contrib.contenttypes.models import ContentType
 

	
 
from symposion.proposals.models import ProposalSection
 

	
 

	
 
class Command(BaseCommand):
 
    
 

 
    def handle(self, *args, **options):
 
        ct, created = ContentType.objects.get_or_create(
 
            model="",
 
            app_label="reviews",
 
            defaults={"name": "reviews"}
 
        )
 
        
 

 
        for ps in ProposalSection.objects.all():
 
            for action in ["review", "manage"]:
 
                perm, created = Permission.objects.get_or_create(
 
                    codename="can_%s_%s" % (action, ps.section.slug),
 
                    content_type__pk=ct.id,
 
                    defaults={"name": "Can %s %s" % (action, ps), "content_type": ct}
symposion/reviews/management/commands/promoteproposals.py
Show inline comments
...
 
@@ -2,14 +2,15 @@ from django.core.management.base import BaseCommand
 
from django.db import connections
 

	
 
from symposion.reviews.models import ProposalResult, promote_proposal
 

	
 

	
 
class Command(BaseCommand):
 
    
 

 
    def handle(self, *args, **options):
 
        accepted_proposals = ProposalResult.objects.filter(status="accepted")
 
        accepted_proposals = accepted_proposals.order_by("proposal")
 
        
 

 
        for result in accepted_proposals:
 
            promote_proposal(result.proposal)
 
        connections["default"].cursor().execute("SELECT setval('schedule_session_id_seq', (SELECT max(id) FROM schedule_session))")
 
        connections["default"].cursor().execute(
 
            "SELECT setval('schedule_session_id_seq', (SELECT max(id) FROM schedule_session))")
symposion/reviews/models.py
Show inline comments
...
 
@@ -12,27 +12,27 @@ from markitup.fields import MarkupField
 

	
 
from symposion.proposals.models import ProposalBase
 
from symposion.schedule.models import Presentation
 

	
 

	
 
class ProposalScoreExpression(object):
 
    
 

 
    def as_sql(self, qn, connection=None):
 
        sql = "((3 * plus_one + plus_zero) - (minus_zero + 3 * minus_one))"
 
        return sql, []
 
    
 

 
    def prepare_database_save(self, unused):
 
        return self
 

	
 

	
 
class Votes(object):
 
    PLUS_ONE = "+1"
 
    PLUS_ZERO = "+0"
 
    MINUS_ZERO = u"−0"
 
    MINUS_ONE = u"−1"
 
    
 

 
    CHOICES = [
 
        (PLUS_ONE, u"+1 — Good proposal and I will argue for it to be accepted."),
 
        (PLUS_ZERO, u"+0 — OK proposal, but I will not argue for it to be accepted."),
 
        (MINUS_ZERO, u"−0 — Weak proposal, but I will not argue strongly against acceptance."),
 
        (MINUS_ONE, u"−1 — Serious issues and I will argue to reject this proposal."),
 
    ]
...
 
@@ -42,27 +42,27 @@ VOTES = Votes()
 
class ReviewAssignment(models.Model):
 
    AUTO_ASSIGNED_INITIAL = 0
 
    OPT_IN = 1
 
    AUTO_ASSIGNED_LATER = 2
 

	
 
    NUM_REVIEWERS = 3
 
    
 

 
    ORIGIN_CHOICES = [
 
        (AUTO_ASSIGNED_INITIAL, "auto-assigned, initial"),
 
        (OPT_IN, "opted-in"),
 
        (AUTO_ASSIGNED_LATER, "auto-assigned, later"),
 
    ]
 
    
 

 
    proposal = models.ForeignKey("proposals.ProposalBase")
 
    user = models.ForeignKey(User)
 
    
 

 
    origin = models.IntegerField(choices=ORIGIN_CHOICES)
 
    
 

 
    assigned_at = models.DateTimeField(default=datetime.now)
 
    opted_out = models.BooleanField()
 
    
 

 
    @classmethod
 
    def create_assignments(cls, proposal, origin=AUTO_ASSIGNED_INITIAL):
 
        speakers = [proposal.speaker] + list(proposal.additional_speakers.all())
 
        reviewers = User.objects.exclude(
 
            pk__in=[
 
                speaker.user_id
...
 
@@ -91,49 +91,49 @@ class ReviewAssignment(models.Model):
 
            )
 

	
 

	
 
class ProposalMessage(models.Model):
 
    proposal = models.ForeignKey("proposals.ProposalBase", related_name="messages")
 
    user = models.ForeignKey(User)
 
    
 

 
    message = MarkupField()
 
    submitted_at = models.DateTimeField(default=datetime.now, editable=False)
 

	
 
    class Meta:
 
        ordering = ["submitted_at"]
 

	
 

	
 
class Review(models.Model):
 
    VOTES = VOTES
 
    
 

 
    proposal = models.ForeignKey("proposals.ProposalBase", related_name="reviews")
 
    user = models.ForeignKey(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, blank=True, choices=VOTES.CHOICES)
 
    comment = MarkupField()
 
    submitted_at = models.DateTimeField(default=datetime.now, editable=False)
 
    
 

 
    def save(self, **kwargs):
 
        if self.vote:
 
            vote, created = LatestVote.objects.get_or_create(
 
                proposal = self.proposal,
 
                user = self.user,
 
                defaults = dict(
 
                    vote = self.vote,
 
                    submitted_at = self.submitted_at,
 
                proposal=self.proposal,
 
                user=self.user,
 
                defaults=dict(
 
                    vote=self.vote,
 
                    submitted_at=self.submitted_at,
 
                )
 
            )
 
            if not created:
 
                LatestVote.objects.filter(pk=vote.pk).update(vote=self.vote)
 
                self.proposal.result.update_vote(self.vote, previous=vote.vote)
 
            else:
 
                self.proposal.result.update_vote(self.vote)
 
        super(Review, self).save(**kwargs)
 
    
 

 
    def delete(self):
 
        model = self.__class__
 
        user_reviews = model._default_manager.filter(
 
            proposal=self.proposal,
 
            user=self.user,
 
        )
...
 
@@ -149,13 +149,14 @@ class Review(models.Model):
 
        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]
 
                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,
 
                )
...
 
@@ -163,40 +164,40 @@ class Review(models.Model):
 
                # 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.PLUS_ONE: "plus-one",
 
            self.VOTES.PLUS_ZERO: "plus-zero",
 
            self.VOTES.MINUS_ZERO: "minus-zero",
 
            self.VOTES.MINUS_ONE: "minus-one",
 
        }[self.vote]
 
    
 

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

	
 

	
 
class LatestVote(models.Model):
 
    VOTES = VOTES
 
    
 

 
    proposal = models.ForeignKey("proposals.ProposalBase", related_name="votes")
 
    user = models.ForeignKey(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)
 
    submitted_at = models.DateTimeField(default=datetime.now, editable=False)
 
    
 

 
    class Meta:
 
        unique_together = [("proposal", "user")]
 
    
 

 
    def css_class(self):
 
        return {
 
            self.VOTES.PLUS_ONE: "plus-one",
 
            self.VOTES.PLUS_ZERO: "plus-zero",
 
            self.VOTES.MINUS_ZERO: "minus-zero",
 
            self.VOTES.MINUS_ONE: "minus-one",
...
 
@@ -220,38 +221,38 @@ class ProposalResult(models.Model):
 
    status = models.CharField(max_length=20, choices=[
 
        ("accepted", "accepted"),
 
        ("rejected", "rejected"),
 
        ("undecided", "undecided"),
 
        ("standby", "standby"),
 
    ], default="undecided")
 
    
 

 
    @classmethod
 
    def full_calculate(cls):
 
        for proposal in ProposalBase.objects.all():
 
            result, created = cls._default_manager.get_or_create(proposal=proposal)
 
            result.comment_count = Review.objects.filter(proposal=proposal).count()
 
            result.vote_count = LatestVote.objects.filter(proposal=proposal).count()
 
            result.plus_one = LatestVote.objects.filter(
 
                proposal = proposal,
 
                vote = VOTES.PLUS_ONE
 
                proposal=proposal,
 
                vote=VOTES.PLUS_ONE
 
            ).count()
 
            result.plus_zero = LatestVote.objects.filter(
 
                proposal = proposal,
 
                vote = VOTES.PLUS_ZERO
 
                proposal=proposal,
 
                vote=VOTES.PLUS_ZERO
 
            ).count()
 
            result.minus_zero = LatestVote.objects.filter(
 
                proposal = proposal,
 
                vote = VOTES.MINUS_ZERO
 
                proposal=proposal,
 
                vote=VOTES.MINUS_ZERO
 
            ).count()
 
            result.minus_one = LatestVote.objects.filter(
 
                proposal = proposal,
 
                vote = VOTES.MINUS_ONE
 
                proposal=proposal,
 
                vote=VOTES.MINUS_ONE
 
            ).count()
 
            result.save()
 
            cls._default_manager.filter(pk=result.pk).update(score=ProposalScoreExpression())
 
    
 

 
    def update_vote(self, vote, previous=None, removal=False):
 
        mapping = {
 
            VOTES.PLUS_ONE: "plus_one",
 
            VOTES.PLUS_ZERO: "plus_zero",
 
            VOTES.MINUS_ZERO: "minus_zero",
 
            VOTES.MINUS_ONE: "minus_one",
...
 
@@ -280,62 +281,63 @@ class ProposalResult(models.Model):
 

	
 

	
 
class Comment(models.Model):
 
    proposal = models.ForeignKey("proposals.ProposalBase", related_name="comments")
 
    commenter = models.ForeignKey(User)
 
    text = MarkupField()
 
    
 

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

	
 

	
 
class NotificationTemplate(models.Model):
 
    
 

 
    label = models.CharField(max_length=100)
 
    from_address = models.EmailField()
 
    subject = models.CharField(max_length=100)
 
    body = models.TextField()
 

	
 

	
 
class ResultNotification(models.Model):
 
    
 

 
    proposal = models.ForeignKey("proposals.ProposalBase", related_name="notifications")
 
    template = models.ForeignKey(NotificationTemplate, null=True, blank=True, on_delete=models.SET_NULL)
 
    template = models.ForeignKey(NotificationTemplate, null=True, blank=True,
 
                                 on_delete=models.SET_NULL)
 
    timestamp = models.DateTimeField(default=datetime.now)
 
    to_address = models.EmailField()
 
    from_address = models.EmailField()
 
    subject = models.CharField(max_length=100)
 
    body = models.TextField()
 
    
 

 
    @property
 
    def email_args(self):
 
        return (self.subject, self.body, self.from_address, [self.to_address])
 

	
 

	
 
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,
 
            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()
symposion/reviews/templatetags/review_tags.py
Show inline comments
 
from django import template
 

	
 
from symposion.reviews.models import Review, ReviewAssignment
 
from symposion.reviews.models import ReviewAssignment
 

	
 

	
 
register = template.Library()
 

	
 

	
 
@register.assignment_tag(takes_context=True)
symposion/reviews/tests.py
Show inline comments
...
 
@@ -12,70 +12,70 @@ class login(object):
 
        self.testcase = testcase
 
        success = testcase.client.login(username=user, password=password)
 
        self.testcase.assertTrue(
 
            success,
 
            "login with username=%r, password=%r failed" % (user, password)
 
        )
 
    
 

 
    def __enter__(self):
 
        pass
 
    
 

 
    def __exit__(self, *args):
 
        self.testcase.client.logout()
 

	
 

	
 
class ReviewTests(TestCase):
 
    fixtures = ["proposals"]
 
    
 

 
    def get(self, url_name, *args, **kwargs):
 
        return self.client.get(reverse(url_name, args=args, kwargs=kwargs))
 
    
 

 
    def post(self, url_name, *args, **kwargs):
 
        data = kwargs.pop("data")
 
        return self.client.post(reverse(url_name, args=args, kwargs=kwargs), data)
 
    
 

 
    def login(self, user, password):
 
        return login(self, user, password)
 
    
 

 
    def test_detail_perms(self):
 
        guidos_proposal = Proposal.objects.all()[0]
 
        response = self.get("review_detail", pk=guidos_proposal.pk)
 
        
 

 
        # Not logged in
 
        self.assertEqual(response.status_code, 302)
 
        
 

 
        with self.login("guido", "pythonisawesome"):
 
            response = self.get("review_detail", pk=guidos_proposal.pk)
 
            # Guido can see his own proposal.
 
            self.assertEqual(response.status_code, 200)
 
        
 

 
        with self.login("matz", "pythonsucks"):
 
            response = self.get("review_detail", pk=guidos_proposal.pk)
 
            # Matz can't see guido's proposal
 
            self.assertEqual(response.status_code, 302)
 
        
 

 
        larry = User.objects.get(username="larryw")
 
        # Larry is a trustworthy guy, he's a reviewer.
 
        larry.groups.add(Group.objects.get(name="reviewers"))
 
        with self.login("larryw", "linenoisehere"):
 
            response = self.get("review_detail", pk=guidos_proposal.pk)
 
            # Reviewers can see a review detail page.
 
            self.assertEqual(response.status_code, 200)
 
    
 

 
    def test_reviewing(self):
 
        guidos_proposal = Proposal.objects.all()[0]
 
        
 

 
        with self.login("guido", "pythonisawesome"):
 
            response = self.post("review_review", pk=guidos_proposal.pk, data={
 
                "vote": "+1",
 
            })
 
            # It redirects, but...
 
            self.assertEqual(response.status_code, 302)
 
            # ... no vote recorded
 
            self.assertEqual(guidos_proposal.reviews.count(), 0)
 
        
 

 
        larry = User.objects.get(username="larryw")
 
        # Larry is a trustworthy guy, he's a reviewer.
 
        larry.groups.add(Group.objects.get(name="reviewers"))
 
        with self.login("larryw", "linenoisehere"):
 
            response = self.post("review_review", pk=guidos_proposal.pk, data={
 
                "vote": "+0",
...
 
@@ -87,57 +87,57 @@ class ReviewTests(TestCase):
 
            assignment = ReviewAssignment.objects.get()
 
            self.assertEqual(assignment.proposal, guidos_proposal)
 
            self.assertEqual(assignment.origin, ReviewAssignment.OPT_IN)
 
            self.assertEqual(guidos_proposal.comments.count(), 1)
 
            comment = guidos_proposal.comments.get()
 
            self.assertFalse(comment.public)
 
            
 

 
            response = self.post("review_review", pk=guidos_proposal.pk, data={
 
                "vote": "+1",
 
                "text": "Actually Perl is dead, we really need a talk on the future",
 
            })
 
            self.assertEqual(guidos_proposal.reviews.count(), 2)
 
            self.assertEqual(ReviewAssignment.objects.count(), 1)
 
            assignment = ReviewAssignment.objects.get()
 
            self.assertEqual(assignment.review, Review.objects.order_by("-id")[0])
 
            self.assertEqual(guidos_proposal.comments.count(), 2)
 
            
 

 
            # Larry's a big fan...
 
            response = self.post("review_review", pk=guidos_proposal.pk, data={
 
                "vote": "+20",
 
            })
 
            self.assertEqual(guidos_proposal.reviews.count(), 2)
 
    
 

 
    def test_speaker_commenting(self):
 
        guidos_proposal = Proposal.objects.all()[0]
 
        
 

 
        with self.login("guido", "pythonisawesome"):
 
            response = self.get("review_comment", pk=guidos_proposal.pk)
 
            # Guido can comment on his proposal.
 
            self.assertEqual(response.status_code, 200)
 
            
 

 
            response = self.post("review_comment", pk=guidos_proposal.pk, data={
 
                "text": "FYI I can do this as a 30-minute or 45-minute talk.",
 
            })
 
            self.assertEqual(response.status_code, 302)
 
            self.assertEqual(guidos_proposal.comments.count(), 1)
 
            comment = guidos_proposal.comments.get()
 
            self.assertTrue(comment.public)
 
        
 

 
        larry = User.objects.get(username="larryw")
 
        # Larry is a trustworthy guy, he's a reviewer.
 
        larry.groups.add(Group.objects.get(name="reviewers"))
 
        with self.login("larryw", "linenoisehere"):
 
            response = self.get("review_comment", pk=guidos_proposal.pk)
 
            # Larry can comment, since he's a reviewer
 
            self.assertEqual(response.status_code, 200)
 
            
 

 
            response = self.post("review_comment", pk=guidos_proposal.pk, data={
 
                "text": "Thanks for the heads-up Guido."
 
            })
 
            self.assertEqual(response.status_code, 302)
 
            self.assertEqual(guidos_proposal.comments.count(), 2)
 
        
 

 
        with self.login("matz", "pythonsucks"):
 
            response = self.get("review_comment", pk=guidos_proposal.pk)
 
            # Matz can't comment.
 
            self.assertEqual(response.status_code, 302)
symposion/reviews/urls.py
Show inline comments
 
# flake8: noqa
 
from django.conf.urls.defaults import patterns, url
 

	
 

	
 
urlpatterns = patterns("symposion.reviews.views",
 
    url(r"^section/(?P<section_slug>[\w\-]+)/all/$", "review_section", {"reviewed": "all"}, name="review_section"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/reviewed/$", "review_section", {"reviewed": "reviewed"}, name="user_reviewed"),
...
 
@@ -11,13 +12,13 @@ urlpatterns = patterns("symposion.reviews.views",
 
    url(r"^section/(?P<section_slug>[\w\-]+)/list/(?P<user_pk>\d+)/$", "review_list", name="review_list_user"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/admin/$", "review_admin", name="review_admin"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/admin/accept/$", "review_bulk_accept", name="review_bulk_accept"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/notification/(?P<status>\w+)/$", "result_notification", name="result_notification"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/notification/(?P<status>\w+)/prepare/$", "result_notification_prepare", name="result_notification_prepare"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/notification/(?P<status>\w+)/send/$", "result_notification_send", name="result_notification_send"),
 
    
 

 
    url(r"^review/(?P<pk>\d+)/$", "review_detail", name="review_detail"),
 
    
 

 
    url(r"^(?P<pk>\d+)/delete/$", "review_delete", name="review_delete"),
 
    url(r"^assignments/$", "review_assignments", name="review_assignments"),
 
    url(r"^assignment/(?P<pk>\d+)/opt-out/$", "review_assignment_opt_out", name="review_assignment_opt_out"),
 
)
symposion/reviews/utils.py
Show inline comments
 
def has_permission(user, proposal, speaker=False, reviewer=False):
 
    """
 
    Returns whether or not ther user has permission to review this proposal,
 
    with the specified requirements.
 
    
 
    If ``speaker`` is ``True`` then the user can be one of the speakers for the 
 

	
 
    If ``speaker`` is ``True`` then the user can be one of the speakers for the
 
    proposal.  If ``reviewer`` is ``True`` the speaker can be a part of the
 
    reviewer group.
 
    """
 
    if user.is_superuser:
 
        return True
 
    if speaker:
 
        if (user == proposal.speaker.user or 
 
            proposal.additional_speakers.filter(user=user).exists()):
 
        if user == proposal.speaker.user or \
 
           proposal.additional_speakers.filter(user=user).exists():
 
            return True
 
    if reviewer:
 
        if user.groups.filter(name="reviewers").exists():
 
            return True
 
    return False
symposion/reviews/views.py
Show inline comments
 
import re
 

	
 
from django.core.mail import send_mass_mail
 
from django.db.models import Q
 
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed
 
from django.shortcuts import render, redirect, get_object_or_404
 
from django.template import Context, Template
 
from django.views.decorators.http import require_POST
...
 
@@ -24,257 +22,259 @@ from symposion.reviews.models import (
 

	
 
def access_not_permitted(request):
 
    return render(request, "reviews/access_not_permitted.html")
 

	
 

	
 
def proposals_generator(request, queryset, user_pk=None, check_speaker=True):
 
    
 

 
    for obj in queryset:
 
        # @@@ this sucks; we can do better
 
        if check_speaker:
 
            if request.user in [s.user for s in obj.speakers()]:
 
                continue
 
        
 

 
        try:
 
            obj.result
 
        except ProposalResult.DoesNotExist:
 
            ProposalResult.objects.get_or_create(proposal=obj)
 
        
 

 
        obj.comment_count = obj.result.comment_count
 
        obj.total_votes = obj.result.vote_count
 
        obj.plus_one = obj.result.plus_one
 
        obj.plus_zero = obj.result.plus_zero
 
        obj.minus_zero = obj.result.minus_zero
 
        obj.minus_one = obj.result.minus_one
 
        lookup_params = dict(proposal=obj)
 
        
 

 
        if user_pk:
 
            lookup_params["user__pk"] = user_pk
 
        else:
 
            lookup_params["user"] = request.user
 
        
 

 
        try:
 
            obj.user_vote = LatestVote.objects.get(**lookup_params).vote
 
            obj.user_vote_css = LatestVote.objects.get(**lookup_params).css_class()
 
        except LatestVote.DoesNotExist:
 
            obj.user_vote = None
 
            obj.user_vote_css = "no-vote"
 
        
 

 
        yield obj
 

	
 

	
 
# Returns a list of all proposals, proposals reviewed by the user, or the proposals the user has yet to review
 
# depending on the link user clicks in dashboard
 
# Returns a list of all proposals, proposals reviewed by the user, or the proposals the user has
 
# yet to review depending on the link user clicks in dashboard
 
@login_required
 
def review_section(request, section_slug, assigned=False, reviewed="all"):
 
    
 

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

 
    section = get_object_or_404(ProposalSection, section__slug=section_slug)
 
    queryset = ProposalBase.objects.filter(kind__section=section)
 
    
 

 
    if assigned:
 
        assignments = ReviewAssignment.objects.filter(user=request.user).values_list("proposal__id")
 
        assignments = ReviewAssignment.objects.filter(user=request.user)\
 
            .values_list("proposal__id")
 
        queryset = queryset.filter(id__in=assignments)
 
    
 

 
    # passing reviewed in from reviews.urls and out to review_list for
 
    # appropriate template header rendering
 
    if reviewed == "all":
 
        queryset = queryset.select_related("result").select_subclasses()
 
        reviewed = "all_reviews"
 
    elif reviewed == "reviewed":
 
        queryset = queryset.filter(reviews__user=request.user)
 
        reviewed = "user_reviewed"
 
    else:
 
        queryset = queryset.exclude(reviews__user=request.user).exclude(speaker=request.user)
 
        reviewed = "user_not_reviewed"
 
    
 

 
    proposals = proposals_generator(request, queryset)
 
    
 

 
    ctx = {
 
        "proposals": proposals,
 
        "section": section,
 
        "reviewed": reviewed,
 
    }
 
    
 

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

	
 

	
 
@login_required
 
def review_list(request, section_slug, user_pk):
 
    
 

 
    # if they're not a reviewer admin and they aren't the person whose
 
    # review list is being asked for, don't let them in
 
    if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
 
        if not request.user.pk == user_pk:
 
            return access_not_permitted(request)
 
    
 

 
    queryset = ProposalBase.objects.select_related("speaker__user", "result")
 
    reviewed = LatestVote.objects.filter(user__pk=user_pk).values_list("proposal", flat=True)
 
    queryset = queryset.filter(pk__in=reviewed)
 
    proposals = queryset.order_by("submitted")
 
    
 

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

 
    proposals = proposals_generator(request, proposals, user_pk=user_pk, check_speaker=not admin)
 
    
 

 
    ctx = {
 
        "proposals": proposals,
 
    }
 
    return render(request, "reviews/review_list.html", ctx)
 

	
 

	
 
@login_required
 
def review_admin(request, section_slug):
 
    
 

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

 
    def reviewers():
 
        already_seen = set()
 
        
 

 
        for team in Team.objects.filter(permissions__codename="can_review_%s" % section_slug):
 
            for membership in team.memberships.filter(Q(state="member") | Q(state="manager")):
 
                user = membership.user
 
                if user.pk in already_seen:
 
                    continue
 
                already_seen.add(user.pk)
 
                
 

 
                user.comment_count = Review.objects.filter(user=user).count()
 
                user.total_votes = LatestVote.objects.filter(user=user).count()
 
                user.plus_one = LatestVote.objects.filter(
 
                    user = user,
 
                    vote = LatestVote.VOTES.PLUS_ONE
 
                    user=user,
 
                    vote=LatestVote.VOTES.PLUS_ONE
 
                ).count()
 
                user.plus_zero = LatestVote.objects.filter(
 
                    user = user,
 
                    vote = LatestVote.VOTES.PLUS_ZERO
 
                    user=user,
 
                    vote=LatestVote.VOTES.PLUS_ZERO
 
                ).count()
 
                user.minus_zero = LatestVote.objects.filter(
 
                    user = user,
 
                    vote = LatestVote.VOTES.MINUS_ZERO
 
                    user=user,
 
                    vote=LatestVote.VOTES.MINUS_ZERO
 
                ).count()
 
                user.minus_one = LatestVote.objects.filter(
 
                    user = user,
 
                    vote = LatestVote.VOTES.MINUS_ONE
 
                    user=user,
 
                    vote=LatestVote.VOTES.MINUS_ONE
 
                ).count()
 
                
 

 
                yield user
 
    
 

 
    ctx = {
 
        "section_slug": section_slug,
 
        "reviewers": reviewers(),
 
    }
 
    return render(request, "reviews/review_admin.html", ctx)
 

	
 

	
 
@login_required
 
def review_detail(request, pk):
 
    
 

 
    proposals = ProposalBase.objects.select_related("result").select_subclasses()
 
    proposal = get_object_or_404(proposals, pk=pk)
 
    
 

 
    if not request.user.has_perm("reviews.can_review_%s" % proposal.kind.section.slug):
 
        return access_not_permitted(request)
 
    
 

 
    speakers = [s.user for s in proposal.speakers()]
 
    
 

 
    if not request.user.is_superuser and request.user in speakers:
 
        return access_not_permitted(request)
 
    
 

 
    admin = request.user.is_staff
 
    
 

 
    try:
 
        latest_vote = LatestVote.objects.get(proposal=proposal, user=request.user)
 
    except LatestVote.DoesNotExist:
 
        latest_vote = None
 
    
 

 
    if request.method == "POST":
 
        if request.user in speakers:
 
            return access_not_permitted(request)
 
        
 

 
        if "vote_submit" in request.POST:
 
            review_form = ReviewForm(request.POST)
 
            if review_form.is_valid():
 
                
 

 
                review = review_form.save(commit=False)
 
                review.user = request.user
 
                review.proposal = proposal
 
                review.save()
 
                
 

 
                return redirect(request.path)
 
            else:
 
                message_form = SpeakerCommentForm()
 
        elif "message_submit" in request.POST:
 
            message_form = SpeakerCommentForm(request.POST)
 
            if message_form.is_valid():
 
                
 

 
                message = message_form.save(commit=False)
 
                message.user = request.user
 
                message.proposal = proposal
 
                message.save()
 
                
 

 
                for speaker in speakers:
 
                    if speaker and speaker.email:
 
                        ctx = {
 
                            "proposal": proposal,
 
                            "message": message,
 
                            "reviewer": False,
 
                        }
 
                        send_email(
 
                            [speaker.email], "proposal_new_message",
 
                            context = ctx
 
                            context=ctx
 
                        )
 
                
 

 
                return redirect(request.path)
 
            else:
 
                initial = {}
 
                if latest_vote:
 
                    initial["vote"] = latest_vote.vote
 
                if request.user in speakers:
 
                    review_form = None
 
                else:
 
                    review_form = ReviewForm(initial=initial)
 
        elif "result_submit" in request.POST:
 
            if admin:
 
                result = request.POST["result_submit"]
 
                
 

 
                if result == "accept":
 
                    proposal.result.status = "accepted"
 
                    proposal.result.save()
 
                elif result == "reject":
 
                    proposal.result.status = "rejected"
 
                    proposal.result.save()
 
                elif result == "undecide":
 
                    proposal.result.status = "undecided"
 
                    proposal.result.save()
 
                elif result == "standby":
 
                    proposal.result.status = "standby"
 
                    proposal.result.save()
 
            
 

 
            return redirect(request.path)
 
    else:
 
        initial = {}
 
        if latest_vote:
 
            initial["vote"] = latest_vote.vote
 
        if request.user in speakers:
 
            review_form = None
 
        else:
 
            review_form = ReviewForm(initial=initial)
 
        message_form = SpeakerCommentForm()
 
    
 

 
    proposal.comment_count = proposal.result.comment_count
 
    proposal.total_votes = proposal.result.vote_count
 
    proposal.plus_one = proposal.result.plus_one
 
    proposal.plus_zero = proposal.result.plus_zero
 
    proposal.minus_zero = proposal.result.minus_zero
 
    proposal.minus_one = proposal.result.minus_one
 
    
 

 
    reviews = Review.objects.filter(proposal=proposal).order_by("-submitted_at")
 
    messages = proposal.messages.order_by("submitted_at")
 
    
 

 
    return render(request, "reviews/review_detail.html", {
 
        "proposal": proposal,
 
        "latest_vote": latest_vote,
 
        "reviews": reviews,
 
        "review_messages": messages,
 
        "review_form": review_form,
...
 
@@ -284,67 +284,78 @@ 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.slug
 
    
 

 
    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)
 
    
 

 
    VOTE_THRESHOLD = settings.SYMPOSION_VOTE_THRESHOLD
 
    
 

 
    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 = {
 
        # proposals with at least VOTE_THRESHOLD reviews and at least one +1 and no -1s, sorted by the 'score'
 
        "positive": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__plus_one__gt=0, result__minus_one=0).order_by("-result__score"),
 
        # proposals with at least VOTE_THRESHOLD reviews and at least one -1 and no +1s, reverse sorted by the 'score'
 
        "negative": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_one__gt=0, result__plus_one=0).order_by("result__score"),
 
        # proposals with at least VOTE_THRESHOLD reviews and neither a +1 or a -1, sorted by total votes (lowest first)
 
        "indifferent": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_one=0, result__plus_one=0).order_by("result__vote_count"),
 
        # proposals with at least VOTE_THRESHOLD reviews and both a +1 and -1, sorted by total votes (highest first)
 
        "controversial": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__plus_one__gt=0, result__minus_one__gt=0).order_by("-result__vote_count"),
 
        # proposals with at least VOTE_THRESHOLD reviews and at least one +1 and no -1s, sorted by
 
        # the 'score'
 
        "positive": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__plus_one__gt=0,
 
                                    result__minus_one=0).order_by("-result__score"),
 
        # proposals with at least VOTE_THRESHOLD reviews and at least one -1 and no +1s, reverse
 
        # sorted by the 'score'
 
        "negative": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_one__gt=0,
 
                                    result__plus_one=0).order_by("result__score"),
 
        # proposals with at least VOTE_THRESHOLD reviews and neither a +1 or a -1, sorted by total
 
        # votes (lowest first)
 
        "indifferent": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_one=0,
 
                                       result__plus_one=0).order_by("result__vote_count"),
 
        # proposals with at least VOTE_THRESHOLD reviews and both a +1 and -1, sorted by total
 
        # votes (highest first)
 
        "controversial": queryset.filter(result__vote_count__gte=VOTE_THRESHOLD,
 
                                         result__plus_one__gt=0, result__minus_one__gt=0)
 
        .order_by("-result__vote_count"),
 
        # proposals with fewer than VOTE_THRESHOLD reviews
 
        "too_few": queryset.filter(result__vote_count__lt=VOTE_THRESHOLD).order_by("result__vote_count"),
 
        "too_few": queryset.filter(result__vote_count__lt=VOTE_THRESHOLD)
 
        .order_by("result__vote_count"),
 
    }
 
    
 

 
    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))
 
    
 
        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, "reviews/review_stats.html", ctx)
 

	
 

	
 
@login_required
 
def review_assignments(request):
 
    if not request.user.groups.filter(name="reviewers").exists():
...
 
@@ -358,20 +369,19 @@ def review_assignments(request):
 
    })
 

	
 

	
 
@login_required
 
@require_POST
 
def review_assignment_opt_out(request, pk):
 
    review_assignment = get_object_or_404(ReviewAssignment,
 
        pk=pk,
 
        user=request.user
 
    )
 
    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)
 
        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):
...
 
@@ -384,26 +394,28 @@ def review_bulk_accept(request, section_slug):
 
            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, "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()
 

	
 
    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,
 
    }
...
 
@@ -411,16 +423,16 @@ def result_notification(request, section_slug, status):
 

	
 

	
 
@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()
...
 
@@ -428,19 +440,19 @@ def result_notification_prepare(request, section_slug, status):
 
        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]),
...
 
@@ -449,40 +461,40 @@ def result_notification_prepare(request, section_slug, status):
 

	
 

	
 
@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"]
...
 
@@ -491,10 +503,10 @@ def result_notification_send(request, section_slug, status):
 
            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/forms.py
Show inline comments
...
 
@@ -4,22 +4,22 @@ from django.db.models import Q
 
from markitup.widgets import MarkItUpWidget
 

	
 
from symposion.schedule.models import Presentation
 

	
 

	
 
class SlotEditForm(forms.Form):
 
    
 

 
    def __init__(self, *args, **kwargs):
 
        self.slot = kwargs.pop("slot")
 
        super(SlotEditForm, self).__init__(*args, **kwargs)
 
        # @@@ TODO - Make this configurable
 
        if self.slot.kind.label in ["talk", "tutorial", "keynote"]:
 
            self.fields["presentation"] = self.build_presentation_field()
 
        else:
 
            self.fields["content_override"] = self.build_content_override_field()
 
    
 

 
    def build_presentation_field(self):
 
        kwargs = {}
 
        queryset = Presentation.objects.all()
 
        queryset = queryset.exclude(cancelled=True)
 
        queryset = queryset.order_by("proposal_base__pk")
 
        if self.slot.content:
...
 
@@ -28,13 +28,13 @@ class SlotEditForm(forms.Form):
 
            kwargs["initial"] = self.slot.content
 
        else:
 
            queryset = queryset.filter(slot=None)
 
            kwargs["required"] = True
 
        kwargs["queryset"] = queryset
 
        return forms.ModelChoiceField(**kwargs)
 
    
 

 
    def build_content_override_field(self):
 
        kwargs = {
 
            "label": "Content",
 
            "widget": MarkItUpWidget(),
 
            "required": False,
 
            "initial": self.slot.content_override,
symposion/schedule/models.py
Show inline comments
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.db import models
 

	
 
from markitup.fields import MarkupField
 
from model_utils.managers import InheritanceManager
 

	
 
from symposion.proposals.models import ProposalBase
 
from symposion.conference.models import Section
 

	
 

	
 
class Schedule(models.Model):
 
    
 

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

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

 
    class Meta:
 
        ordering = ["section"]
 

	
 

	
 
class Day(models.Model):
 
    
 

 
    schedule = models.ForeignKey(Schedule)
 
    date = models.DateField()
 
    
 

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

 
    class Meta:
 
        unique_together = [("schedule", "date")]
 
        ordering = ["date"]
 

	
 

	
 
class Room(models.Model):
 
    
 

 
    schedule = models.ForeignKey(Schedule)
 
    name = models.CharField(max_length=65)
 
    order = models.PositiveIntegerField()
 
    
 

 
    def __unicode__(self):
 
        return self.name
 

	
 

	
 
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)
 
    label = models.CharField(max_length=50)
 
    
 

 
    def __unicode__(self):
 
        return self.label
 

	
 

	
 
class Slot(models.Model):
 
    
 

 
    day = models.ForeignKey(Day)
 
    kind = models.ForeignKey(SlotKind)
 
    start = models.TimeField()
 
    end = models.TimeField()
 
    content_override = MarkupField(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.
 
        """
 
        if self.content and self.content.slot_id:
 
            self.content.slot = None
 
            self.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 rooms(self):
 
        return Room.objects.filter(pk__in=self.slotroom_set.values("room"))
 
    
 

 
    def __unicode__(self):
 
        return "%s %s (%s - %s)" % (self.day, self.kind, self.start, self.end)
 
    
 

 
    class Meta:
 
        ordering = ["day", "start", "end"]
 

	
 

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

 
    slot = models.ForeignKey(Slot)
 
    room = models.ForeignKey(Room)
 
    
 

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

 
    class Meta:
 
        unique_together = [("slot", "room")]
 
        ordering = ["slot", "room__order"]
 

	
 

	
 
class Presentation(models.Model):
 
    
 

 
    slot = models.OneToOneField(Slot, null=True, blank=True, related_name="content_ptr")
 
    title = models.CharField(max_length=100)
 
    description = MarkupField()
 
    abstract = MarkupField()
 
    speaker = models.ForeignKey("speakers.Speaker", related_name="presentations")
 
    additional_speakers = models.ManyToManyField("speakers.Speaker", related_name="copresentations", blank=True)
 
    additional_speakers = models.ManyToManyField("speakers.Speaker", related_name="copresentations",
 
                                                 blank=True)
 
    cancelled = models.BooleanField(default=False)
 
    proposal_base = models.OneToOneField(ProposalBase, related_name="presentation")
 
    section = models.ForeignKey(Section, related_name="presentations")
 
    
 

 
    @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 __unicode__(self):
 
        return "#%s %s (%s)" % (self.number, self.title, self.speaker)
 
    
 

 
    class Meta:
 
        ordering = ["slot"]
symposion/schedule/timetable.py
Show inline comments
 
import itertools
 
import operator
 

	
 
from django.db.models import Count, Min
 

	
 
from symposion.schedule.models import Room, Slot, SlotRoom
 

	
 

	
 
class TimeTable(object):
 
    
 

 
    def __init__(self, day):
 
        self.day = day
 
    
 

 
    def slots_qs(self):
 
        qs = Slot.objects.all()
 
        qs = qs.filter(day=self.day)
 
        return qs
 
    
 

 
    def rooms(self):
 
        qs = Room.objects.all()
 
        qs = qs.filter(schedule=self.day.schedule)
 
        qs = qs.filter(pk__in=SlotRoom.objects.filter(slot__in=self.slots_qs().values("pk")).values("room"))
 
        qs = qs.filter(
 
            pk__in=SlotRoom.objects.filter(slot__in=self.slots_qs().values("pk")).values("room"))
 
        qs = qs.order_by("order")
 
        return qs
 
    
 

 
    def __iter__(self):
 
        times = sorted(set(itertools.chain(*self.slots_qs().values_list("start", "end"))))
 
        slots = Slot.objects.filter(pk__in=self.slots_qs().values("pk"))
 
        slots = slots.annotate(room_count=Count("slotroom"), order=Min("slotroom__room__order"))
 
        slots = slots.order_by("start", "order")
 
        row = []
...
 
@@ -35,13 +35,13 @@ class TimeTable(object):
 
                if slot.start == time:
 
                    slot.rowspan = TimeTable.rowspan(times, slot.start, slot.end)
 
                    slot.colspan = slot.room_count
 
                    row["slots"].append(slot)
 
            if row["slots"] or next_time is None:
 
                yield row
 
    
 

 
    @staticmethod
 
    def rowspan(times, start, end):
 
        return times.index(end) - times.index(start)
 

	
 

	
 
def pairwise(iterable):
symposion/schedule/urls.py
Show inline comments
 
# flake8: noqa
 
from django.conf.urls.defaults import url, patterns
 

	
 

	
 
urlpatterns = patterns("symposion.schedule.views",
 
    url(r"^$", "schedule_conference", name="schedule_conference"),
 
    url(r"^edit/$", "schedule_edit", name="schedule_edit"),
symposion/schedule/views.py
Show inline comments
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.http import Http404, HttpResponse
 
from django.shortcuts import render, get_object_or_404, redirect
 
from django.template import loader, Context
 

	
 
from django.contrib.auth.decorators import login_required
 

	
...
 
@@ -9,118 +8,118 @@ from symposion.schedule.forms import SlotEditForm
 
from symposion.schedule.models import Schedule, Day, Slot, Presentation
 
from symposion.schedule.timetable import TimeTable
 

	
 

	
 
def fetch_schedule(slug):
 
    qs = Schedule.objects.all()
 
    
 

 
    if slug is None:
 
        if qs.count() > 1:
 
            raise Http404()
 
        schedule = next(iter(qs), None)
 
        if schedule is None:
 
            raise Http404()
 
    else:
 
        schedule = get_object_or_404(qs, section__slug=slug)
 
    
 

 
    return schedule
 

	
 

	
 
def schedule_conference(request):
 
    
 

 
    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, "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, "schedule/schedule_detail.html", ctx)
 

	
 

	
 
def schedule_list(request, slug=None):
 
    schedule = fetch_schedule(slug)
 
    
 

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

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

	
 

	
 
def schedule_list_csv(request, slug=None):
 
    schedule = fetch_schedule(slug)
 
    
 

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

 
    response = HttpResponse(mimetype="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("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)
 
    
 

 
    days_qs = Day.objects.filter(schedule=schedule)
 
    days = [TimeTable(day) for day in days_qs]
 
    ctx = {
 
        "schedule": schedule,
 
        "days": days,
 
    }
 
    return render(request, "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"]
...
 
@@ -142,18 +141,18 @@ def schedule_slot_edit(request, slug, slot_pk):
 
            "slot": slot,
 
        }
 
        return render(request, "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
 
    else:
 
        schedule = None
 
    
 

 
    ctx = {
 
        "presentation": presentation,
 
        "schedule": schedule,
 
    }
 
    return render(request, "schedule/presentation_detail.html", ctx)
symposion/speakers/admin.py
Show inline comments
 
from django.contrib import admin
 

	
 
from symposion.speakers.models import Speaker
 

	
 

	
 
admin.site.register(Speaker,
 
    list_display = ["name", "email", "created"],
 
    search_fields = ["name"],
 
)
...
 
\ No newline at end of file
 
                    list_display=["name", "email", "created"],
 
                    search_fields=["name"])
symposion/speakers/fixture_gen.py
Show inline comments
...
 
@@ -7,23 +7,23 @@ from symposion.speakers.models import Speaker
 

	
 
@fixture_generator(Speaker, User)
 
def speakers():
 
    guido = User.objects.create_user("guido", "guido@python.org", "pythonisawesome")
 
    matz = User.objects.create_user("matz", "matz@ruby.org", "pythonsucks")
 
    larry = User.objects.create_user("larryw", "larry@perl.org", "linenoisehere")
 
    
 

 
    Speaker.objects.create(
 
        user=guido,
 
        name="Guido van Rossum",
 
        biography="I wrote Python, and named it after Monty Python",
 
    )
 
    Speaker.objects.create(
 
        user=matz,
 
        name="Yukihiro Matsumoto",
 
        biography="I wrote Ruby, and named it after the rare gem Ruby, a pun "
 
            "on Perl/pearl.",
 
        biography=("I wrote Ruby, and named it after the rare gem Ruby, a pun "
 
                   "on Perl/pearl."),
 
    )
 
    Speaker.objects.create(
 
        user=larry,
 
        name="Larry Wall",
 
        biography="I wrote Perl, and named it after the Parable of the Pearl",
 
    )
symposion/speakers/forms.py
Show inline comments
...
 
@@ -3,13 +3,13 @@ from django import forms
 
from markitup.widgets import MarkItUpWidget
 

	
 
from symposion.speakers.models import Speaker
 

	
 

	
 
class SpeakerForm(forms.ModelForm):
 
    
 

 
    class Meta:
 
        model = Speaker
 
        fields = [
 
            "name",
 
            "biography",
 
            "photo",
symposion/speakers/management/commands/export_speaker_data.py
Show inline comments
 
import csv
 
import os
 

	
 
from django.core.management.base import BaseCommand, CommandError
 
from django.core.management.base import BaseCommand
 

	
 
from symposion.speakers.models import Speaker
 

	
 

	
 
class Command(BaseCommand):
 
    
 

 
    def handle(self, *args, **options):
 
        csv_file = csv.writer(open(os.path.join(os.getcwd(), "speakers.csv"), "wb"))
 
        csv_file.writerow(["Name", "Bio"])
 
        
 

 
        for speaker in Speaker.objects.all():
 
            csv_file.writerow([
 
                speaker.name.encode("utf-8"),
 
                speaker.biography.encode("utf-8"),
 
            ])
symposion/speakers/models.py
Show inline comments
...
 
@@ -6,50 +6,53 @@ from django.core.urlresolvers import reverse
 
from django.contrib.auth.models import User
 

	
 
from markitup.fields import MarkupField
 

	
 

	
 
class Speaker(models.Model):
 
    
 

 
    SESSION_COUNT_CHOICES = [
 
        (1, "One"),
 
        (2, "Two")
 
    ]
 
    
 

 
    user = models.OneToOneField(User, null=True, related_name="speaker_profile")
 
    name = models.CharField(max_length=100, help_text="As you would like it to appear in the conference program.")
 
    biography = MarkupField(blank=True, help_text="A little bit about you. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>.")
 
    name = models.CharField(max_length=100, help_text=("As you would like it to appear in the "
 
                                                       "conference program."))
 
    biography = MarkupField(blank=True, help_text=("A little bit about you.  Edit using "
 
                                                   "<a href='http://warpedvisions.org/projects/"
 
                                                   "markdown-cheat-sheet/target='_blank'>"
 
                                                   "Markdown</a>."))
 
    photo = models.ImageField(upload_to="speaker_photos", blank=True)
 
    annotation = models.TextField()  # staff only
 
    invite_email = models.CharField(max_length=200, unique=True, null=True, db_index=True)
 
    invite_token = models.CharField(max_length=40, db_index=True)
 
    created = models.DateTimeField(
 
        default = datetime.datetime.now,
 
        editable = False
 
        default=datetime.datetime.now,
 
        editable=False
 
    )
 

	
 
    class Meta:
 
        ordering = ['name']
 
    
 

 
    def __unicode__(self):
 
        if self.user:
 
            return self.name
 
        else:
 
            return "?"
 

	
 
    
 
    def get_absolute_url(self):
 
        return reverse("speaker_edit")
 
    
 

 
    @property
 
    def email(self):
 
        if self.user is not None:
 
            return self.user.email
 
        else:
 
            return self.invite_email
 
    
 

 
    @property
 
    def all_presentations(self):
 
        presentations = []
 
        if self.presentations:
 
            for p in self.presentations.all():
 
                presentations.append(p)
symposion/speakers/urls.py
Show inline comments
 
# flake8: noqa
 
from django.conf.urls.defaults import *
 

	
 

	
 
urlpatterns = patterns("symposion.speakers.views",
 
    url(r"^create/$", "speaker_create", name="speaker_create"),
 
    url(r"^create/(\w+)/$", "speaker_create_token", name="speaker_create_token"),
symposion/speakers/views.py
Show inline comments
...
 
@@ -14,61 +14,61 @@ from symposion.speakers.models import Speaker
 
@login_required
 
def speaker_create(request):
 
    try:
 
        return redirect(request.user.speaker_profile)
 
    except ObjectDoesNotExist:
 
        pass
 
    
 

 
    if request.method == "POST":
 
        try:
 
            speaker = Speaker.objects.get(invite_email=request.user.email)
 
            found = True
 
        except Speaker.DoesNotExist:
 
            speaker = None
 
            found = False
 
        form = SpeakerForm(request.POST, request.FILES, instance=speaker)
 
        
 

 
        if form.is_valid():
 
            speaker = form.save(commit=False)
 
            speaker.user = request.user
 
            if not found:
 
                speaker.invite_email = None
 
            speaker.save()
 
            messages.success(request, "Speaker profile created.")
 
            return redirect("dashboard")
 
    else:
 
        form = SpeakerForm(initial={"name": request.user.get_full_name()})
 
    
 

 
    return render(request, "speakers/speaker_create.html", {
 
        "form": form,
 
    })
 

	
 

	
 
@login_required
 
def speaker_create_staff(request, pk):
 
    user = get_object_or_404(User, pk=pk)
 
    if not request.user.is_staff:
 
        raise Http404
 
    
 

 
    try:
 
        return redirect(user.speaker_profile)
 
    except ObjectDoesNotExist:
 
        pass
 
    
 

 
    if request.method == "POST":
 
        form = SpeakerForm(request.POST, request.FILES)
 
        
 

 
        if form.is_valid():
 
            speaker = form.save(commit=False)
 
            speaker.user = user
 
            speaker.save()
 
            messages.success(request, "Speaker profile created.")
 
            return redirect("user_list")
 
    else:
 
        form = SpeakerForm(initial={"name": user.get_full_name()})
 
    
 

 
    return render(request, "speakers/speaker_create.html", {
 
        "form": form,
 
    })
 

	
 

	
 
def speaker_create_token(request, token):
...
 
@@ -85,14 +85,14 @@ def speaker_create_token(request, token):
 
            additional_speakers = ProposalBase.additional_speakers.through
 
            additional_speakers._default_manager.filter(
 
                speaker=speaker
 
            ).update(
 
                speaker=existing_speaker
 
            )
 
            messages.info(request, "You have been associated with all pending "
 
                "talk proposals")
 
            messages.info(request, ("You have been associated with all pending "
 
                                    "talk proposals"))
 
            return redirect("dashboard")
 
    else:
 
        if not request.user.is_authenticated():
 
            return redirect("account_login")
 
    return redirect("speaker_create")
 

	
...
 
@@ -106,31 +106,31 @@ def speaker_edit(request, pk=None):
 
            return redirect("speaker_create")
 
    else:
 
        if request.user.is_staff:
 
            speaker = get_object_or_404(Speaker, pk=pk)
 
        else:
 
            raise Http404()
 
    
 

 
    if request.method == "POST":
 
        form = SpeakerForm(request.POST, request.FILES, instance=speaker)
 
        if form.is_valid():
 
            form.save()
 
            messages.success(request, "Speaker profile updated.")
 
            return redirect("dashboard")
 
    else:
 
        form = SpeakerForm(instance=speaker)
 
    
 

 
    return render(request, "speakers/speaker_edit.html", {
 
        "form": form,
 
    })
 

	
 

	
 
def speaker_profile(request, pk):
 
    speaker = get_object_or_404(Speaker, pk=pk)
 
    presentations = speaker.all_presentations
 
    if not presentations and not request.user.is_staff:
 
        raise Http404()
 
    
 

 
    return render(request, "speakers/speaker_profile.html", {
 
        "speaker": speaker,
 
        "presentations": presentations,
 
    })
symposion/sponsorship/admin.py
Show inline comments
 
from django.contrib import admin
 

	
 
from symposion.sponsorship.models import SponsorLevel, Sponsor, Benefit, BenefitLevel, SponsorBenefit
 
from symposion.sponsorship.models import SponsorLevel, Sponsor, Benefit, BenefitLevel, \
 
    SponsorBenefit
 

	
 

	
 
class BenefitLevelInline(admin.TabularInline):
 
    model = BenefitLevel
 
    extra = 0
 

	
...
 
@@ -21,13 +22,13 @@ class SponsorBenefitInline(admin.StackedInline):
 
            ]
 
        })
 
    ]
 

	
 

	
 
class SponsorAdmin(admin.ModelAdmin):
 
    
 

 
    save_on_top = True
 
    fieldsets = [
 
        (None, {
 
            "fields": [
 
                ("name", "applicant"),
 
                ("level", "active"),
...
 
@@ -40,32 +41,32 @@ class SponsorAdmin(admin.ModelAdmin):
 
            "fields": ["added"],
 
            "classes": ["collapse"]
 
        })
 
    ]
 
    inlines = [SponsorBenefitInline]
 
    list_display = ["name", "external_url", "level", "active"]
 
    
 

 
    def get_form(self, *args, **kwargs):
 
        # @@@ kinda ugly but using choices= on NullBooleanField is broken
 
        form = super(SponsorAdmin, self).get_form(*args, **kwargs)
 
        form.base_fields["active"].widget.choices = [
 
            (u"1", "unreviewed"),
 
            (u"2", "approved"),
 
            (u"3", "rejected")
 
        ]
 
        return form
 

	
 

	
 
class BenefitAdmin(admin.ModelAdmin):
 
    
 

 
    list_display = ["name", "type", "description"]
 
    inlines = [BenefitLevelInline]
 

	
 

	
 
class SponsorLevelAdmin(admin.ModelAdmin):
 
    
 

 
    inlines = [BenefitLevelInline]
 

	
 

	
 
admin.site.register(SponsorLevel, SponsorLevelAdmin)
 
admin.site.register(Sponsor, SponsorAdmin)
 
admin.site.register(Benefit, BenefitAdmin)
symposion/sponsorship/forms.py
Show inline comments
...
 
@@ -13,23 +13,23 @@ class SponsorApplicationForm(forms.ModelForm):
 
            "initial": {
 
                "contact_name": self.user.get_full_name,
 
                "contact_email": self.user.email,
 
            }
 
        })
 
        super(SponsorApplicationForm, self).__init__(*args, **kwargs)
 
    
 

 
    class Meta:
 
        model = Sponsor
 
        fields = [
 
            "name",
 
            "external_url",
 
            "contact_name",
 
            "contact_email",
 
            "level"
 
        ]
 
    
 

 
    def save(self, commit=True):
 
        obj = super(SponsorApplicationForm, self).save(commit=False)
 
        obj.applicant = self.user
 
        if commit:
 
            obj.save()
 
        return obj
...
 
@@ -44,32 +44,32 @@ class SponsorDetailsForm(forms.ModelForm):
 
            "contact_name",
 
            "contact_email"
 
        ]
 

	
 

	
 
class SponsorBenefitsInlineFormSet(BaseInlineFormSet):
 
    
 

 
    def _construct_form(self, i, **kwargs):
 
        form = super(SponsorBenefitsInlineFormSet, self)._construct_form(i, **kwargs)
 
        
 

 
        # only include the relevant data fields for this benefit type
 
        fields = form.instance.data_fields()
 
        form.fields = dict((k, v) for (k, v) in form.fields.items() if k in fields + ["id"])
 
        
 

 
        for field in fields:
 
            # don't need a label, the form template will label it with the benefit name
 
            form.fields[field].label = ""
 
            
 

 
            # provide word limit as help_text
 
            if form.instance.benefit.type == "text" and form.instance.max_words:
 
                form.fields[field].help_text = u"maximum %s words" % form.instance.max_words
 
            
 

 
            # use admin file widget that shows currently uploaded file
 
            if field == "upload":
 
                form.fields[field].widget = AdminFileWidget()
 
        
 

 
        return form
 

	
 

	
 
SponsorBenefitsFormSet = inlineformset_factory(
 
    Sponsor, SponsorBenefit,
 
    formset=SponsorBenefitsInlineFormSet,
symposion/sponsorship/management/commands/reset_sponsor_benefits.py
Show inline comments
 
from django.core.management.base import BaseCommand
 

	
 
from django.contrib.auth.models import Group
 

	
 
from symposion.sponsorship.models import Sponsor, SponsorBenefit
 
from symposion.sponsorship.models import Sponsor, SponsorBenefit, SponsorLevel
 

	
 

	
 
class Command(BaseCommand):
 
    
 

 
    def handle(self, *args, **options):
 
        for sponsor in Sponsor.objects.all():
 
            level = None  
 
            level = None
 
            try:
 
                level = sponsor.level
 
            except SponsorLevel.DoesNotExist:
 
                pass
 
            if level:
 
                for benefit_level in level.benefit_levels.all():
 
                    # Create all needed benefits if they don't exist already
 
                    sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
 
                        sponsor=sponsor, benefit=benefit_level.benefit)
 
                    
 

 
                    if created:
 
                        print "created", sponsor_benefit, "for", sponsor
 
                    
 

 
                    # and set to default limits for this level.
 
                    sponsor_benefit.max_words = benefit_level.max_words
 
                    sponsor_benefit.other_limits = benefit_level.other_limits
 
                    
 

 
                    # and set to active
 
                    sponsor_benefit.active = True
 
                    
 

 
                    # @@@ We don't call sponsor_benefit.clean here. This means
 
                    # that if the sponsorship level for a sponsor is adjusted
 
                    # downwards, an existing too-long text entry can remain,
 
                    # and won't raise a validation error until it's next
 
                    # edited.
 
                    sponsor_benefit.save()
symposion/sponsorship/models.py
Show inline comments
...
 
@@ -11,120 +11,124 @@ from django.contrib.auth.models import User
 
from symposion.conference.models import Conference
 

	
 
from symposion.sponsorship.managers import SponsorManager
 

	
 

	
 
class SponsorLevel(models.Model):
 
    
 

 
    conference = models.ForeignKey(Conference, verbose_name=_("conference"))
 
    name = models.CharField(_("name"), max_length=100)
 
    order = models.IntegerField(_("order"), default=0)
 
    cost = models.PositiveIntegerField(_("cost"))
 
    description = models.TextField(_("description"), blank=True, help_text=_("This is private."))
 
    
 

 
    class Meta:
 
        ordering = ["conference", "order"]
 
        verbose_name = _("sponsor level")
 
        verbose_name_plural = _("sponsor levels")
 
    
 

 
    def __unicode__(self):
 
        return self.name
 
    
 

 
    def sponsors(self):
 
        return self.sponsor_set.filter(active=True).order_by("added")
 

	
 

	
 
class Sponsor(models.Model):
 
    
 
    applicant = models.ForeignKey(User, related_name="sponsorships", verbose_name=_("applicant"), null=True)
 
    
 

	
 
    applicant = models.ForeignKey(User, related_name="sponsorships", verbose_name=_("applicant"),
 
                                  null=True)
 

	
 
    name = models.CharField(_("Sponsor Name"), max_length=100)
 
    external_url = models.URLField(_("external URL"))
 
    annotation = models.TextField(_("annotation"), blank=True)
 
    contact_name = models.CharField(_("Contact Name"), max_length=100)
 
    contact_email = models.EmailField(_(u"Contact Email"))
 
    level = models.ForeignKey(SponsorLevel, verbose_name=_("level"))
 
    added = models.DateTimeField(_("added"), default=datetime.datetime.now)
 
    active = models.BooleanField(_("active"), default=False)
 
    
 

 
    # Denormalization (this assumes only one logo)
 
    sponsor_logo = models.ForeignKey("SponsorBenefit", related_name="+", null=True, blank=True, editable=False)
 
    
 
    sponsor_logo = models.ForeignKey("SponsorBenefit", related_name="+", null=True, blank=True,
 
                                     editable=False)
 

	
 
    objects = SponsorManager()
 
    
 

 
    def __unicode__(self):
 
        return self.name
 
    
 

 
    class Meta:
 
        verbose_name = _("sponsor")
 
        verbose_name_plural = _("sponsors")
 
    
 

 
    def get_absolute_url(self):
 
        if self.active:
 
            return reverse("sponsor_detail", kwargs={"pk": self.pk})
 
        return reverse("sponsor_list")
 
    
 

 
    @property
 
    def website_logo(self):
 
        if self.sponsor_logo is None:
 
            benefits = self.sponsor_benefits.filter(benefit__type="weblogo", upload__isnull=False)[:1]
 
            benefits = self.sponsor_benefits.filter(
 
                benefit__type="weblogo", upload__isnull=False)[:1]
 
            if benefits.count():
 
                if benefits[0].upload:
 
                    self.sponsor_logo = benefits[0]
 
                    self.save()
 
        return self.sponsor_logo.upload
 
    
 

 
    @property
 
    def listing_text(self):
 
        if not hasattr(self, "_listing_text"):
 
            self._listing_text = None
 
            # @@@ better than hard-coding a pk but still not good
 
            benefits = self.sponsor_benefits.filter(benefit__name="Sponsor Description")
 
            if benefits.count():
 
                self._listing_text = benefits[0].text
 
        return self._listing_text
 
    
 

 
    def reset_benefits(self):
 
        """
 
        Reset all benefits for this sponsor to the defaults for their
 
        sponsorship level.
 
        """
 
        level = None
 
        
 

 
        try:
 
            level = self.level
 
        except SponsorLevel.DoesNotExist:
 
            pass
 
        
 

 
        allowed_benefits = []
 
        if level:
 
            for benefit_level in level.benefit_levels.all():
 
                # Create all needed benefits if they don't exist already
 
                sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
 
                    sponsor=self, benefit=benefit_level.benefit)
 
                
 

 
                # and set to default limits for this level.
 
                sponsor_benefit.max_words = benefit_level.max_words
 
                sponsor_benefit.other_limits = benefit_level.other_limits
 
                
 

 
                # and set to active
 
                sponsor_benefit.active = True
 
                
 

 
                # @@@ We don't call sponsor_benefit.clean here. This means
 
                # that if the sponsorship level for a sponsor is adjusted
 
                # downwards, an existing too-long text entry can remain,
 
                # and won't raise a validation error until it's next
 
                # edited.
 
                sponsor_benefit.save()
 
                
 

 
                allowed_benefits.append(sponsor_benefit.pk)
 
        
 

 
        # Any remaining sponsor benefits that don't normally belong to
 
        # this level are set to inactive
 
        self.sponsor_benefits.exclude(pk__in=allowed_benefits).update(active=False, max_words=None, other_limits="")
 
    
 
        self.sponsor_benefits.exclude(pk__in=allowed_benefits)\
 
            .update(active=False, max_words=None, other_limits="")
 

	
 
    def send_coordinator_emails(self):
 
        pass  # @@@ should this just be done centrally?
 

	
 

	
 
def _store_initial_level(sender, instance, **kwargs):
 
    if instance:
...
 
@@ -144,65 +148,66 @@ BENEFIT_TYPE_CHOICES = [
 
    ("weblogo", "Web Logo"),
 
    ("simple", "Simple")
 
]
 

	
 

	
 
class Benefit(models.Model):
 
    
 

 
    name = models.CharField(_("name"), max_length=100)
 
    description = models.TextField(_("description"), blank=True)
 
    type = models.CharField(_("type"), choices=BENEFIT_TYPE_CHOICES, max_length=10, default="simple")
 
    
 
    type = models.CharField(_("type"), choices=BENEFIT_TYPE_CHOICES, max_length=10,
 
                            default="simple")
 

	
 
    def __unicode__(self):
 
        return self.name
 

	
 

	
 
class BenefitLevel(models.Model):
 
    
 

 
    benefit = models.ForeignKey(Benefit, related_name="benefit_levels", verbose_name=_("benefit"))
 
    level = models.ForeignKey(SponsorLevel, related_name="benefit_levels", verbose_name=_("level"))
 
    
 

 
    # default limits for this benefit at given level
 
    max_words = models.PositiveIntegerField(_("max words"), blank=True, null=True)
 
    other_limits = models.CharField(_("other limits"), max_length=200, blank=True)
 
    
 

 
    class Meta:
 
        ordering = ["level"]
 
    
 

 
    def __unicode__(self):
 
        return u"%s - %s" % (self.level, self.benefit)
 

	
 

	
 
class SponsorBenefit(models.Model):
 
    
 

 
    sponsor = models.ForeignKey(Sponsor, related_name="sponsor_benefits", verbose_name=_("sponsor"))
 
    benefit = models.ForeignKey(Benefit, related_name="sponsor_benefits", verbose_name=_("benefit"))
 
    active = models.BooleanField(default=True)
 
    
 

 
    # Limits: will initially be set to defaults from corresponding BenefitLevel
 
    max_words = models.PositiveIntegerField(_("max words"), blank=True, null=True)
 
    other_limits = models.CharField(_("other limits"), max_length=200, blank=True)
 
    
 

 
    # Data: zero or one of these fields will be used, depending on the
 
    # type of the Benefit (text, file, or simple)
 
    text = models.TextField(_("text"), blank=True)
 
    upload = models.FileField(_("file"), blank=True, upload_to="sponsor_files")
 
    
 

 
    class Meta:
 
        ordering = ["-active"]
 
    
 

 
    def __unicode__(self):
 
        return u"%s - %s" % (self.sponsor, self.benefit)
 
    
 

 
    def clean(self):
 
        num_words = len(self.text.split())
 
        if self.max_words and num_words > self.max_words:
 
            raise ValidationError(
 
                "Sponsorship level only allows for %s words, you provided %d." % (
 
                    self.max_words, num_words))
 
    
 

 
    def data_fields(self):
 
        """
 
        Return list of data field names which should be editable for
 
        this ``SponsorBenefit``, depending on its ``Benefit`` type.
 
        """
 
        if self.benefit.type == "file" or self.benefit.type == "weblogo":
symposion/sponsorship/templatetags/sponsorship_tags.py
Show inline comments
...
 
@@ -5,54 +5,57 @@ from symposion.sponsorship.models import Sponsor, SponsorLevel
 

	
 

	
 
register = template.Library()
 

	
 

	
 
class SponsorsNode(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])
 
        elif len(bits) == 4 and bits[2] == "as":
 
            return cls(bits[3], bits[1])
 
        else:
 
            raise template.TemplateSyntaxError("%r takes 'as var' or 'level as var'" % bits[0])
 
    
 

 
    def __init__(self, context_var, level=None):
 
        if level:
 
            self.level = template.Variable(level)
 
        else:
 
            self.level = None
 
        self.context_var = context_var
 
    
 

 
    def render(self, context):
 
        conference = current_conference()
 
        if self.level:
 
            level = self.level.resolve(context)
 
            queryset = Sponsor.objects.filter(level__conference = conference, level__name__iexact = level, active = True).order_by("added")
 
            queryset = Sponsor.objects.filter(
 
                level__conference=conference, level__name__iexact=level, active=True)\
 
                .order_by("added")
 
        else:
 
            queryset = Sponsor.objects.filter(level__conference = conference, active = True).order_by("level__order", "added")
 
            queryset = Sponsor.objects.filter(level__conference=conference, active=True)\
 
                .order_by("level__order", "added")
 
        context[self.context_var] = queryset
 
        return u""
 

	
 

	
 
class SponsorLevelNode(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):
 
        conference = current_conference()
 
        context[self.context_var] = SponsorLevel.objects.filter(conference=conference)
 
        return u""
 

	
 

	
...
 
@@ -69,7 +72,6 @@ def sponsors(parser, token):
 
@register.tag
 
def sponsor_levels(parser, token):
 
    """
 
    {% sponsor_levels as levels %}
 
    """
 
    return SponsorLevelNode.handle_token(parser, token)
 
    
...
 
\ No newline at end of file
symposion/sponsorship/urls.py
Show inline comments
 
# flake8: noqa
 
from django.conf.urls.defaults import patterns, url
 
from django.views.generic.simple import direct_to_template
 

	
 

	
 
urlpatterns = patterns("symposion.sponsorship.views",
 
    url(r"^$", direct_to_template, {"template": "sponsorship/list.html"}, name="sponsor_list"),
symposion/sponsorship/views.py
Show inline comments
...
 
@@ -2,78 +2,79 @@ from django.http import Http404
 
from django.shortcuts import render_to_response, redirect, get_object_or_404
 
from django.template import RequestContext
 

	
 
from django.contrib import messages
 
from django.contrib.auth.decorators import login_required
 

	
 
from symposion.sponsorship.forms import SponsorApplicationForm, SponsorDetailsForm, SponsorBenefitsFormSet
 
from symposion.sponsorship.forms import SponsorApplicationForm, SponsorDetailsForm, \
 
    SponsorBenefitsFormSet
 
from symposion.sponsorship.models import Sponsor, SponsorBenefit
 

	
 

	
 
@login_required
 
def sponsor_apply(request):
 
    if request.method == "POST":
 
        form = SponsorApplicationForm(request.POST, user=request.user)
 
        if form.is_valid():
 
            sponsor = form.save()
 
            return redirect("sponsor_detail", pk=sponsor.pk)
 
    else:
 
        form = SponsorApplicationForm(user=request.user)
 
    
 

 
    return render_to_response("sponsorship/apply.html", {
 
        "form": form,
 
    }, context_instance=RequestContext(request))
 

	
 

	
 
@login_required
 
def sponsor_add(request):
 
    if not request.user.is_staff:
 
        raise Http404()
 
    
 

 
    if request.method == "POST":
 
        form = SponsorApplicationForm(request.POST, user=request.user)
 
        if form.is_valid():
 
            sponsor = form.save(commit=False)
 
            sponsor.active = True
 
            sponsor.save()
 
            return redirect("sponsor_detail", pk=sponsor.pk)
 
    else:
 
        form = SponsorApplicationForm(user=request.user)
 
    
 

 
    return render_to_response("sponsorship/add.html", {
 
        "form": form,
 
    }, context_instance=RequestContext(request))
 

	
 

	
 
@login_required
 
def sponsor_detail(request, pk):
 
    sponsor = get_object_or_404(Sponsor, pk=pk)
 
    
 

 
    if sponsor.applicant != request.user:
 
        return redirect("sponsor_list")
 
    
 

 
    formset_kwargs = {
 
        "instance": sponsor,
 
        "queryset": SponsorBenefit.objects.filter(active=True)
 
    }
 
    
 

 
    if request.method == "POST":
 
        
 

 
        form = SponsorDetailsForm(request.POST, instance=sponsor)
 
        formset = SponsorBenefitsFormSet(request.POST, request.FILES, **formset_kwargs)
 
        
 

 
        if form.is_valid() and formset.is_valid():
 
            form.save()
 
            formset.save()
 
            
 

 
            messages.success(request, "Sponsorship details have been updated")
 
            
 

 
            return redirect("dashboard")
 
    else:
 
        form = SponsorDetailsForm(instance=sponsor)
 
        formset = SponsorBenefitsFormSet(**formset_kwargs)
 
    
 

 
    return render_to_response("sponsorship/detail.html", {
 
        "sponsor": sponsor,
 
        "form": form,
 
        "formset": formset,
 
    }, context_instance=RequestContext(request))
symposion/teams/admin.py
Show inline comments
...
 
@@ -2,14 +2,13 @@ from django.contrib import admin
 

	
 
import reversion
 

	
 
from symposion.teams.models import Team, Membership
 

	
 
admin.site.register(Team,
 
    prepopulated_fields={"slug": ("name",)},
 
)
 
                    prepopulated_fields={"slug": ("name",)})
 

	
 

	
 
class MembershipAdmin(reversion.VersionAdmin):
 
    list_display = ["team", "user", "state"]
 
    list_filter = ["team"]
 
    search_fields = ["user__username"]
symposion/teams/backends.py
Show inline comments
 
from django.db.models import Q
 

	
 
from .models import Team
 

	
 

	
 
class TeamPermissionsBackend(object):
 
    
 

 
    def authenticate(self, username=None, password=None):
 
        return None
 
    
 

 
    def get_team_permissions(self, user_obj, obj=None):
 
        """
 
        Returns a set of permission strings that this user has through his/her
 
        team memberships.
 
        """
 
        if user_obj.is_anonymous() or obj is not None:
symposion/teams/forms.py
Show inline comments
...
 
@@ -6,46 +6,49 @@ from django.utils.safestring import mark_safe
 
from django.contrib.auth.models import User
 

	
 
from symposion.teams.models import Membership
 

	
 

	
 
class TeamInvitationForm(forms.Form):
 
    
 
    email = forms.EmailField(help_text="email address must be that of an account on this conference site")
 
    
 

	
 
    email = forms.EmailField(help_text=("email address must be that of an account on this "
 
                                        "conference site"))
 

	
 
    def __init__(self, *args, **kwargs):
 
        self.team = kwargs.pop("team")
 
        super(TeamInvitationForm, self).__init__(*args, **kwargs)
 
    
 

 
    def clean(self):
 
        cleaned_data = super(TeamInvitationForm, self).clean()
 
        email = cleaned_data.get("email")
 
        
 

 
        if email is None:
 
            raise forms.ValidationError("valid email address required")
 
        
 

 
        try:
 
            user = User.objects.get(email=email)
 
        except User.DoesNotExist:
 
            # eventually we can invite them but for now assume they are
 
            # already on the site
 
            raise forms.ValidationError(mark_safe("no account with email address <b>%s</b> found on this conference site" % escape(email)))
 
        
 
            raise forms.ValidationError(
 
                mark_safe("no account with email address <b>%s</b> found on this conference "
 
                          "site" % escape(email)))
 

	
 
        state = self.team.get_state_for_user(user)
 
        
 

 
        if state in ["member", "manager"]:
 
            raise forms.ValidationError("user already in team")
 
        
 

 
        if state in ["invited"]:
 
            raise forms.ValidationError("user already invited to team")
 
        
 

 
        self.user = user
 
        self.state = state
 
        
 

 
        return cleaned_data
 
    
 

 
    def invite(self):
 
        if self.state is None:
 
            Membership.objects.create(team=self.team, user=self.user, state="invited")
 
        elif self.state == "applied":
 
            # if they applied we shortcut invitation process
 
            membership = Membership.objects.filter(team=self.team, user=self.user)
symposion/teams/models.py
Show inline comments
...
 
@@ -17,43 +17,44 @@ TEAM_ACCESS_CHOICES = [
 
class Team(models.Model):
 

	
 
    slug = models.SlugField(unique=True)
 
    name = models.CharField(max_length=100)
 
    description = models.TextField(blank=True)
 
    access = models.CharField(max_length=20, choices=TEAM_ACCESS_CHOICES)
 
    
 

 
    # member permissions
 
    permissions = models.ManyToManyField(Permission, blank=True, related_name="member_teams")
 
    
 

 
    # manager permissions
 
    manager_permissions = models.ManyToManyField(Permission, blank=True, related_name="manager_teams")
 
    
 
    manager_permissions = models.ManyToManyField(Permission, blank=True,
 
                                                 related_name="manager_teams")
 

	
 
    created = models.DateTimeField(default=datetime.datetime.now, editable=False)
 
    
 

 
    @models.permalink
 
    def get_absolute_url(self):
 
        return ("team_detail", [self.slug])
 
    
 

 
    def __unicode__(self):
 
        return self.name
 

	
 
    def get_state_for_user(self, user):
 
        try:
 
            return self.memberships.get(user=user).state
 
        except Membership.DoesNotExist:
 
            return None
 
    
 

 
    def applicants(self):
 
        return self.memberships.filter(state="applied")
 
    
 

 
    def invitees(self):
 
        return self.memberships.filter(state="invited")
 
    
 

 
    def members(self):
 
        return self.memberships.filter(state="member")
 
    
 

 
    def managers(self):
 
        return self.memberships.filter(state="manager")
 

	
 

	
 
MEMBERSHIP_STATE_CHOICES = [
 
    ("applied", "applied"),
symposion/teams/templatetags/teams_tags.py
Show inline comments
...
 
@@ -3,24 +3,24 @@ from django import template
 
from symposion.teams.models import Team
 

	
 
register = template.Library()
 

	
 

	
 
class AvailableTeamsNode(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"]
 
        teams = []
 
        for team in Team.objects.all():
 
            state = team.get_state_for_user(request.user)
 
            if team.access == "open" and state is None:
symposion/teams/urls.py
Show inline comments
 
# flake8: noqa
 
from django.conf.urls.defaults import *
 

	
 

	
 
urlpatterns = patterns("symposion.teams.views",
 
    # team specific
 
    url(r"^(?P<slug>[\w\-]+)/$", "team_detail", name="team_detail"),
 
    url(r"^(?P<slug>[\w\-]+)/join/$", "team_join", name="team_join"),
 
    url(r"^(?P<slug>[\w\-]+)/leave/$", "team_leave", name="team_leave"),
 
    url(r"^(?P<slug>[\w\-]+)/apply/$", "team_apply", name="team_apply"),
 
    
 

 
    # membership specific
 
    url(r"^promote/(?P<pk>\d+)/$", "team_promote", name="team_promote"),
 
    url(r"^demote/(?P<pk>\d+)/$", "team_demote", name="team_demote"),
 
    url(r"^accept/(?P<pk>\d+)/$", "team_accept", name="team_accept"),
 
    url(r"^reject/(?P<pk>\d+)/$", "team_reject", name="team_reject"),
 
)
symposion/teams/views.py
Show inline comments
...
 
@@ -7,13 +7,13 @@ from django.contrib import messages
 
from symposion.utils.mail import send_email
 

	
 
from symposion.teams.forms import TeamInvitationForm
 
from symposion.teams.models import Team, Membership
 

	
 

	
 
## perm checks
 
# perm checks
 
#
 
# @@@ these can be moved
 

	
 
def can_join(team, user):
 
    state = team.get_state_for_user(user)
 
    if team.access == "open" and state is None:
...
 
@@ -47,35 +47,35 @@ def can_invite(team, user):
 
    if team.access == "invitation":
 
        if state == "manager" or user.is_staff:
 
            return True
 
    return False
 

	
 

	
 
## views
 
# views
 

	
 

	
 
@login_required
 
def team_detail(request, slug):
 
    team = get_object_or_404(Team, slug=slug)
 
    state = team.get_state_for_user(request.user)
 
    if team.access == "invitation" and state is None and not request.user.is_staff:
 
        raise Http404()
 
    
 

 
    if can_invite(team, request.user):
 
        if request.method == "POST":
 
            form = TeamInvitationForm(request.POST, team=team)
 
            if form.is_valid():
 
                form.invite()
 
                send_email([form.user.email], "teams_user_invited", context={"team": team})
 
                messages.success(request, "Invitation created.")
 
                return redirect("team_detail", slug=slug)
 
        else:
 
            form = TeamInvitationForm(team=team)
 
    else:
 
        form = None
 
    
 

 
    return render(request, "teams/team_detail.html", {
 
        "team": team,
 
        "state": state,
 
        "invite_form": form,
 
        "can_join": can_join(team, request.user),
 
        "can_leave": can_leave(team, request.user),
...
 
@@ -86,13 +86,13 @@ def team_detail(request, slug):
 
@login_required
 
def team_join(request, slug):
 
    team = get_object_or_404(Team, slug=slug)
 
    state = team.get_state_for_user(request.user)
 
    if team.access == "invitation" and state is None and not request.user.is_staff:
 
        raise Http404()
 
    
 

 
    if can_join(team, request.user) and request.method == "POST":
 
        membership, created = Membership.objects.get_or_create(team=team, user=request.user)
 
        membership.state = "member"
 
        membership.save()
 
        messages.success(request, "Joined team.")
 
        return redirect("team_detail", slug=slug)
...
 
@@ -103,13 +103,13 @@ def team_join(request, slug):
 
@login_required
 
def team_leave(request, slug):
 
    team = get_object_or_404(Team, slug=slug)
 
    state = team.get_state_for_user(request.user)
 
    if team.access == "invitation" and state is None and not request.user.is_staff:
 
        raise Http404()
 
    
 

 
    if can_leave(team, request.user) and request.method == "POST":
 
        membership = Membership.objects.get(team=team, user=request.user)
 
        membership.delete()
 
        messages.success(request, "Left team.")
 
        return redirect("dashboard")
 
    else:
...
 
@@ -119,13 +119,13 @@ def team_leave(request, slug):
 
@login_required
 
def team_apply(request, slug):
 
    team = get_object_or_404(Team, slug=slug)
 
    state = team.get_state_for_user(request.user)
 
    if team.access == "invitation" and state is None and not request.user.is_staff:
 
        raise Http404()
 
    
 

 
    if can_apply(team, request.user) and request.method == "POST":
 
        membership, created = Membership.objects.get_or_create(team=team, user=request.user)
 
        membership.state = "applied"
 
        membership.save()
 
        managers = [m.user.email for m in team.managers()]
 
        send_email(managers, "teams_user_applied", context={
symposion/utils/mail.py
Show inline comments
...
 
@@ -4,27 +4,27 @@ from django.template.loader import render_to_string
 
from django.utils.html import strip_tags
 

	
 
from django.contrib.sites.models import Site
 

	
 

	
 
def send_email(to, kind, **kwargs):
 
    
 

 
    current_site = Site.objects.get_current()
 
    
 

 
    ctx = {
 
        "current_site": current_site,
 
        "STATIC_URL": settings.STATIC_URL,
 
    }
 
    ctx.update(kwargs.get("context", {}))
 
    subject = "[%s] %s" % (
 
        current_site.name,
 
        render_to_string("emails/%s/subject.txt" % kind, ctx).strip()
 
    )
 
    
 

 
    message_html = render_to_string("emails/%s/message.html" % kind, ctx)
 
    message_plaintext = strip_tags(message_html)
 
    
 

 
    from_email = settings.DEFAULT_FROM_EMAIL
 
    
 

 
    email = EmailMultiAlternatives(subject, message_plaintext, from_email, to)
 
    email.attach_alternative(message_html, "text/html")
 
    email.send()
symposion/views.py
Show inline comments
...
 
@@ -9,18 +9,18 @@ from django.contrib.auth.decorators import login_required
 
import account.views
 

	
 
import symposion.forms
 

	
 

	
 
class SignupView(account.views.SignupView):
 
    
 

 
    form_class = symposion.forms.SignupForm
 
    form_kwargs = {
 
        "prefix": "signup",
 
    }
 
    
 

 
    def create_user(self, form, commit=True):
 
        user_kwargs = {
 
            "first_name": form.cleaned_data["first_name"],
 
            "last_name": form.cleaned_data["last_name"]
 
        }
 
        return super(SignupView, self).create_user(form, commit=commit, **user_kwargs)
0 comments (0 inline, 0 general)