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
...
 
@@ -4,17 +4,17 @@ from symposion.boxes.utils import load_path_attr
 

	
 

	
 
def default_can_edit(request, *args, **kwargs):
 
    """
 
    This is meant to be overridden in your project per domain specific
 
    requirements.
 
    """
 
    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
 
from django.http import HttpResponseForbidden
 
from django.shortcuts import redirect
 
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
 
            box.created_by = request.user
 
            box.last_updated_by = request.user
 
            box.save()
 
        else:
 
            form.save()
 
        return redirect(next)
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
 
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
...
 
@@ -9,64 +9,65 @@ from django.db import models
 
from django.utils.translation import ugettext_lazy as _
 

	
 
from markitup.fields import MarkupField
 

	
 
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"),
 
    url(r"^files/create/$", "file_create", name="file_create"),
 
    url(r"^files/(\d+)/([^/]+)$", "file_download", name="file_download"),
 
    url(r"^files/(\d+)/delete/$", "file_delete", name="file_delete"),
 
    url(r"^(?P<path>%s)_edit/$" % PAGE_RE, "page_edit", name="cms_page_edit"),
 
    url(r"^(?P<path>%s)$" % PAGE_RE, "page", name="cms_page"),
 
)
symposion/cms/views.py
Show inline comments
...
 
@@ -14,117 +14,117 @@ def can_edit(page, user):
 
        return True
 
    else:
 
        return user.has_perm("cms.change_page")
 

	
 

	
 
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
 
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
...
 
@@ -2,74 +2,74 @@ from django.db import models
 
from django.utils.translation import ugettext_lazy as _
 

	
 
from timezones.fields import TimeZoneField
 

	
 

	
 
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"]
 

	
 

	
 
def current_conference():
 
    from django.conf import settings
 
    try:
 
        conf_id = settings.CONFERENCE_ID
 
    except AttributeError:
 
        from django.core.exceptions import ImproperlyConfigured
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
 
from django.http import Http404
 
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)
 
        del self.fields["username"]
 
        self.fields.keyOrder = [
 
            "email",
 
            "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):
 
        """
 
        Generic csv export admin action.
 
        based on http://djangosnippets.org/snippets/1697/
 
        """
 
        opts = modeladmin.model._meta
 
        if fields:
 
            fieldset = set(fields)
 
            field_names = fieldset
 
        elif exclude:
 
            excludeset = set(exclude)
 
            field_names = field_names - excludeset
 
        response = HttpResponse(mimetype="text/csv")
 
        response["Content-Disposition"] = "attachment; filename=%s.csv" % unicode(opts).replace(".", "_")
 
        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
 
from django import forms
 
from django.db.models import Q
 

	
 
from symposion.proposals.models import SupportingDocument
 
# from markitup.widgets import MarkItUpWidget
 

	
 

	
 
# @@@ generic proposal form
 

	
 

	
 
class AddSpeakerForm(forms.Form):
 
    
 

 
    email = forms.EmailField(
 
        label="Email address of new speaker (use their email address, not yours)"
 
    )
 
    
 

 
    def __init__(self, *args, **kwargs):
 
        self.proposal = kwargs.pop("proposal")
 
        super(AddSpeakerForm, self).__init__(*args, **kwargs)
 
    
 

 
    def clean_email(self):
 
        value = self.cleaned_data["email"]
 
        exists = self.proposal.additional_speakers.filter(
 
            Q(user=None, invite_email=value) |
 
            Q(user__email=value)
 
        ).exists()
 
        if exists:
 
            raise forms.ValidationError(
 
                "This email address has already been invited to your talk proposal"
 
            )
 
        return value
 

	
 

	
 
class SupportingDocumentCreateForm(forms.ModelForm):
 
    
 

 
    class Meta:
 
        model = SupportingDocument
 
        fields = [
 
            "file",
 
            "description",
 
        ]
symposion/proposals/managers.py
Show inline comments
 
deleted file
symposion/proposals/models.py
Show inline comments
...
 
@@ -12,164 +12,174 @@ from django.contrib.auth.models import User
 
import reversion
 

	
 
from markitup.fields import MarkupField
 

	
 
from model_utils.managers import InheritanceManager
 

	
 
from symposion.conference.models import Section
 

	
 

	
 
class ProposalSection(models.Model):
 
    """
 
    configuration of proposal submissions for a specific Section.
 
    
 

 
    a section is available for proposals iff:
 
      * it is after start (if there is one) and
 
      * it is before end (if there is one) and
 
      * closed is NULL or False
 
    """
 
    
 

 
    section = models.OneToOneField(Section)
 
    
 

 
    start = models.DateTimeField(null=True, blank=True)
 
    end = models.DateTimeField(null=True, blank=True)
 
    closed = models.NullBooleanField()
 
    published = models.NullBooleanField()
 
    
 

 
    @classmethod
 
    def available(cls):
 
        now = datetime.datetime.now()
 
        return cls._default_manager.filter(
 
            Q(start__lt=now) | Q(start=None),
 
            Q(end__gt=now) | Q(end=None),
 
            Q(closed=False) | Q(closed=None),
 
        )
 
    
 

 
    def 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
 
from django import template
 

	
 
from symposion.proposals.models import AdditionalSpeaker
 

	
 

	
 
register = template.Library()
 

	
 

	
 
class AssociatedProposalsNode(template.Node):
 
    
 

 
    @classmethod
 
    def handle_token(cls, parser, token):
 
        bits = token.split_contents()
 
        if len(bits) == 3 and bits[1] == "as":
 
            return cls(bits[2])
 
        else:
 
            raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
 
    
 

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

 
    def render(self, context):
 
        request = context["request"]
 
        if request.user.speaker_profile:
 
            pending = AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED
 
            speaker = request.user.speaker_profile
 
            queryset = AdditionalSpeaker.objects.filter(speaker=speaker, status=pending)
 
            context[self.context_var] = [item.proposalbase for item in queryset]
 
        else:
 
            context[self.context_var] = None
 
        return u""
 

	
 

	
 
class PendingProposalsNode(template.Node):
 
    
 

 
    @classmethod
 
    def handle_token(cls, parser, token):
 
        bits = token.split_contents()
 
        if len(bits) == 3 and bits[1] == "as":
 
            return cls(bits[2])
 
        else:
 
            raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
 
    
 

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

 
    def render(self, context):
 
        request = context["request"]
 
        if request.user.speaker_profile:
 
            pending = AdditionalSpeaker.SPEAKING_STATUS_PENDING
 
            speaker = request.user.speaker_profile
 
            queryset = AdditionalSpeaker.objects.filter(speaker=speaker, status=pending)
 
            context[self.context_var] = [item.proposalbase for item in queryset]
 
        else:
 
            context[self.context_var] = None
 
        return u""
 

	
 

	
...
 
@@ -61,13 +61,12 @@ def pending_proposals(parser, token):
 
    """
 
    {% pending_proposals as pending_proposals %}
 
    """
 
    return PendingProposalsNode.handle_token(parser, token)
 

	
 

	
 
@register.tag
 
def associated_proposals(parser, token):
 
    """
 
    {% associated_proposals as associated_proposals %}
 
    """
 
    return AssociatedProposalsNode.handle_token(parser, token)
 

	
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"),
 
    url(r"^(\d+)/$", "proposal_detail", name="proposal_detail"),
 
    url(r"^(\d+)/edit/$", "proposal_edit", name="proposal_edit"),
 
    url(r"^(\d+)/speakers/$", "proposal_speaker_manage", name="proposal_speaker_manage"),
 
    url(r"^(\d+)/cancel/$", "proposal_cancel", name="proposal_cancel"),
 
    url(r"^(\d+)/leave/$", "proposal_leave", name="proposal_leave"),
 
    url(r"^(\d+)/join/$", "proposal_pending_join", name="proposal_pending_join"),
 
    url(r"^(\d+)/decline/$", "proposal_pending_decline", name="proposal_pending_decline"),
 
    
 

 
    url(r"^(\d+)/document/create/$", "document_create", name="proposal_document_create"),
 
    url(r"^document/(\d+)/delete/$", "document_delete", name="proposal_document_delete"),
 
    url(r"^document/(\d+)/([^/]+)$", "document_download", name="proposal_document_download"),
 
)
symposion/proposals/views.py
Show inline comments
...
 
@@ -28,89 +28,89 @@ def get_form(name):
 
    __import__(mod_name)
 
    return getattr(sys.modules[mod_name], form_name)
 

	
 

	
 
def proposal_submit(request):
 
    if not request.user.is_authenticated():
 
        return redirect("home")  # @@@ unauth'd speaker info page?
 
    else:
 
        try:
 
            request.user.speaker_profile
 
        except ObjectDoesNotExist:
 
            return redirect("dashboard")
 
    
 

 
    kinds = []
 
    for proposal_section in ProposalSection.available():
 
        for kind in proposal_section.section.proposal_kinds.all():
 
            kinds.append(kind)
 
    
 

 
    return render(request, "proposals/proposal_submit.html", {
 
        "kinds": kinds,
 
    })
 

	
 

	
 
def proposal_submit_kind(request, kind_slug):
 
    
 

 
    kind = get_object_or_404(ProposalKind, slug=kind_slug)
 
    
 

 
    if not request.user.is_authenticated():
 
        return redirect("home")  # @@@ unauth'd speaker info page?
 
    else:
 
        try:
 
            speaker_profile = request.user.speaker_profile
 
        except ObjectDoesNotExist:
 
            return redirect("dashboard")
 
    
 

 
    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
 
            proposal.save()
 
            form.save_m2m()
 
            messages.success(request, "Proposal submitted.")
 
            if "add-speakers" in request.POST:
 
                return redirect("proposal_speaker_manage", proposal.pk)
 
            return redirect("dashboard")
 
    else:
 
        form = form_class()
 
    
 

 
    return render(request, "proposals/proposal_submit_kind.html", {
 
        "kind": kind,
 
        "form": form,
 
    })
 

	
 

	
 
@login_required
 
def proposal_speaker_manage(request, pk):
 
    queryset = ProposalBase.objects.select_related("speaker")
 
    proposal = get_object_or_404(queryset, pk=pk)
 
    proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
 
    
 

 
    if proposal.speaker != request.user.speaker_profile:
 
        raise Http404()
 
    
 

 
    if request.method == "POST":
 
        add_speaker_form = AddSpeakerForm(request.POST, proposal=proposal)
 
        if add_speaker_form.is_valid():
 
            message_ctx = {
 
                "proposal": proposal,
 
            }
 
            
 

 
            def create_speaker_token(email_address):
 
                # create token and look for an existing speaker to prevent
 
                # duplicate tokens and confusing the pending speaker
 
                try:
 
                    pending = Speaker.objects.get(
 
                        Q(user=None, invite_email=email_address)
 
                    )
 
                except Speaker.DoesNotExist:
 
                    salt = sha_constructor(str(random.random())).hexdigest()[:5]
 
                    token = sha_constructor(salt + email_address).hexdigest()
 
                    pending = Speaker.objects.create(
 
                        invite_email=email_address,
...
 
@@ -126,70 +126,71 @@ def proposal_speaker_manage(request, pk):
 
                # should only be one since we enforce unique email
 
                user = users[0]
 
                message_ctx["user"] = user
 
                # look for speaker profile
 
                try:
 
                    speaker = user.speaker_profile
 
                except ObjectDoesNotExist:
 
                    speaker, token = create_speaker_token(email_address)
 
                    message_ctx["token"] = token
 
                    # fire off email to user to create profile
 
                    send_email(
 
                        [email_address], "speaker_no_profile",
 
                        context = message_ctx
 
                        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,
 
        "speakers": proposal.speakers(),
 
        "add_speaker_form": add_speaker_form,
 
    }
 
    return render(request, "proposals/proposal_speaker_manage.html", ctx)
 

	
 

	
 
@login_required
 
def proposal_edit(request, pk):
 
    queryset = ProposalBase.objects.select_related("speaker")
 
    proposal = get_object_or_404(queryset, pk=pk)
 
    proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
 

	
 
    if request.user != proposal.speaker.user:
 
        raise Http404()
 
    
 

 
    if not proposal.can_edit():
 
        ctx = {
 
            "title": "Proposal editing closed",
 
            "body": "Proposal editing is closed for this session type."
 
        }
 
        return render(request, "proposals/proposal_error.html", ctx)
 
    
 

 
    form_class = get_form(settings.PROPOSAL_FORMS[proposal.kind.slug])
 

	
 
    if request.method == "POST":
 
        form = form_class(request.POST, instance=proposal)
 
        if form.is_valid():
 
            form.save()
 
            if hasattr(proposal, "reviews"):
 
                users = User.objects.filter(
 
                    Q(review__proposal=proposal) |
 
                    Q(proposalmessage__proposal=proposal)
 
                )
 
                users = users.exclude(id=request.user.id).distinct()
...
 
@@ -197,100 +198,100 @@ def proposal_edit(request, pk):
 
                    ctx = {
 
                        "user": request.user,
 
                        "proposal": proposal,
 
                    }
 
                    send_email(
 
                        [user.email], "proposal_updated",
 
                        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
 
def proposal_leave(request, pk):
 
    queryset = ProposalBase.objects.select_related("speaker")
 
    proposal = get_object_or_404(queryset, pk=pk)
 
    proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
 

	
 
    try:
...
 
@@ -302,84 +303,86 @@ def proposal_leave(request, pk):
 
        # @@@ fire off email to submitter and other speakers
 
        messages.success(request, "You are no longer speaking on %s" % proposal.title)
 
        return redirect("dashboard")
 
    ctx = {
 
        "proposal": proposal,
 
    }
 
    return render(request, "proposals/proposal_leave.html", ctx)
 

	
 

	
 
@login_required
 
def proposal_pending_join(request, pk):
 
    proposal = get_object_or_404(ProposalBase, pk=pk)
 
    speaking = get_object_or_404(AdditionalSpeaker, speaker=request.user.speaker_profile, proposalbase=proposal)
 
    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:
 
        return redirect("dashboard")
 

	
 

	
 
@login_required
 
def document_create(request, proposal_pk):
 
    queryset = ProposalBase.objects.select_related("speaker")
 
    proposal = get_object_or_404(queryset, pk=proposal_pk)
 
    proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
 
    
 

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

	
 

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

	
 

	
 
@login_required
 
def document_delete(request, pk):
 
    document = get_object_or_404(SupportingDocument, pk=pk, uploaded_by=request.user)
 
    proposal_pk = document.proposal.pk
 
    
 

 
    if request.method == "POST":
 
        document.delete()
 
    
 

 
    return redirect("proposal_detail", proposal_pk)
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():
 
        if request.user.has_perm("reviews.can_review_%s" % section.section.slug):
 
            sections.append(section)
 
    return {
 
        "review_sections": sections,
 
    }
symposion/reviews/forms.py
Show inline comments
 
from django import forms
 

	
 
from markitup.widgets import MarkItUpWidget
 

	
 
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
 

	
 

	
 
class Command(BaseCommand):
 

	
 
    def handle(self, *args, **options):
 
        for proposal in ProposalBase.objects.filter(cancelled=0):
 
            print "Creating assignments for %s" % (proposal.title,)
 
            ReviewAssignment.create_assignments(proposal)
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
 
from django.core.management.base import BaseCommand
 

	
 
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}
 
                )
 
                print perm
symposion/reviews/management/commands/promoteproposals.py
Show inline comments
 
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
...
 
@@ -6,69 +6,69 @@ from django.db import models
 
from django.db.models import Q
 
from django.db.models.signals import post_save
 

	
 
from django.contrib.auth.models import User
 

	
 
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."),
 
    ]
 
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
 
                for speaker in speakers
 
                if speaker.user_id is not None
 
            ] + [
 
                assignment.user_id
 
                for assignment in ReviewAssignment.objects.filter(
 
                    proposal_id=proposal.id)]
...
 
@@ -85,124 +85,125 @@ class ReviewAssignment(models.Model):
 
            proposal_id=proposal.id, opted_out=0).count()
 
        for reviewer in reviewers[:max(0, cls.NUM_REVIEWERS - num_assigned_reviewers)]:
 
            cls._default_manager.create(
 
                proposal=proposal,
 
                user=reviewer,
 
                origin=origin,
 
            )
 

	
 

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

 
    def css_class(self):
 
        return {
 
            self.VOTES.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",
 
        }[self.vote]
 

	
 

	
 
class ProposalResult(models.Model):
 
    proposal = models.OneToOneField("proposals.ProposalBase", related_name="result")
 
    score = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.00"))
...
 
@@ -214,50 +215,50 @@ class ProposalResult(models.Model):
 
    minus_one = models.PositiveIntegerField(default=0)
 
    accepted = models.NullBooleanField(choices=[
 
        (True, "accepted"),
 
        (False, "rejected"),
 
        (None, "undecided"),
 
    ], default=None)
 
    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",
 
        }
 
        if previous:
 
            if previous == vote:
 
                return
 
            if removal:
 
                setattr(self, mapping[previous], models.F(mapping[previous]) + 1)
...
 
@@ -274,74 +275,75 @@ class ProposalResult(models.Model):
 
        else:
 
            setattr(self, mapping[vote], models.F(mapping[vote]) + 1)
 
            self.comment_count = models.F("comment_count") + 1
 
        self.save()
 
        model = self.__class__
 
        model._default_manager.filter(pk=self.pk).update(score=ProposalScoreExpression())
 

	
 

	
 
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()
 

	
 

	
 
def accepted_proposal(sender, instance=None, **kwargs):
 
    if instance is None:
 
        return
 
    if instance.status == "accepted":
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)
 
def review_assignments(context):
 
    request = context["request"]
 
    assignments = ReviewAssignment.objects.filter(user=request.user)
 
    return assignments
symposion/reviews/tests.py
Show inline comments
...
 
@@ -6,138 +6,138 @@ from django.contrib.auth.models import User, Group
 
from symposion.proposals.models import Proposal
 
from symposion.reviews.models import Review, ReviewAssignment
 

	
 

	
 
class login(object):
 
    def __init__(self, testcase, user, password):
 
        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",
 
                "text": "Looks like a decent proposal, and Guido is a smart guy",
 
            })
 
            self.assertEqual(response.status_code, 302)
 
            self.assertEqual(guidos_proposal.reviews.count(), 1)
 
            self.assertEqual(ReviewAssignment.objects.count(), 1)
 
            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"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/not_reviewed/$", "review_section", {"reviewed": "not_reviewed"}, name="user_not_reviewed"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/assignments/$", "review_section", {"assigned": True}, name="review_section_assignments"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/status/$", "review_status", name="review_status"),
 
    url(r"^section/(?P<section_slug>[\w\-]+)/status/(?P<key>\w+)/$", "review_status", name="review_status"),
 
    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
 

	
 
from django.contrib.auth.decorators import login_required
 

	
 
from symposion.conf import settings
 
from symposion.proposals.models import ProposalBase, ProposalSection
 
from symposion.teams.models import Team
...
 
@@ -18,483 +16,497 @@ from symposion.reviews.forms import ReviewForm, SpeakerCommentForm
 
from symposion.reviews.forms import BulkPresentationForm
 
from symposion.reviews.models import (
 
    ReviewAssignment, Review, LatestVote, ProposalResult, NotificationTemplate,
 
    ResultNotification
 
)
 

	
 

	
 
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,
 
        "message_form": message_form
 
    })
 

	
 

	
 
@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():
 
        return access_not_permitted(request)
 
    assignments = ReviewAssignment.objects.filter(
 
        user=request.user,
 
        opted_out=False
 
    )
 
    return render(request, "reviews/review_assignment.html", {
 
        "assignments": assignments,
 
    })
 

	
 

	
 
@login_required
 
@require_POST
 
def review_assignment_opt_out(request, pk):
 
    review_assignment = get_object_or_404(ReviewAssignment,
 
        pk=pk,
 
        user=request.user
 
    )
 
    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):
 
        return access_not_permitted(request)
 
    if request.method == "POST":
 
        form = BulkPresentationForm(request.POST)
 
        if form.is_valid():
 
            talk_ids = form.cleaned_data["talk_ids"].split(",")
 
            talks = ProposalBase.objects.filter(id__in=talk_ids).select_related("result")
 
            for talk in talks:
 
                talk.result.status = "accepted"
 
                talk.result.save()
 
            return redirect("review_section", section_slug=section_slug)
 
    else:
 
        form = BulkPresentationForm()
 
    
 

 
    return render(request, "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,
 
    }
 
    return render(request, "reviews/result_notification.html", ctx)
 

	
 

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

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

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

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

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

	
 

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

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

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

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

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

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

 
    emails = []
 
    
 

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

 
    send_mass_mail(emails)
 
    
 

 
    return redirect("result_notification", section_slug=section_slug, status=status)
symposion/schedule/forms.py
Show inline comments
 
from django import forms
 
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:
 
            queryset = queryset.filter(Q(slot=None) | Q(pk=self.slot.content.pk))
 
            kwargs["required"] = False
 
            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,
 
        }
 
        return forms.CharField(**kwargs)
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 = []
 
        for time, next_time in pairwise(times):
 
            row = {"time": time, "slots": []}
 
            for slot in slots:
 
                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):
 
    a, b = itertools.tee(iterable)
 
    b.next()
 
    return itertools.izip_longest(a, b)
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"),
 
    url(r"^list/$", "schedule_list", name="schedule_list"),
 
    url(r"^presentations.csv$", "schedule_list_csv", name="schedule_list_csv"),
 
    url(r"^presentation/(\d+)/$", "schedule_presentation_detail", name="schedule_presentation_detail"),
 
    url(r"^([\w\-]+)/$", "schedule_detail", name="schedule_detail"),
 
    url(r"^([\w\-]+)/edit/$", "schedule_edit", name="schedule_edit"),
 
    url(r"^([\w\-]+)/list/$", "schedule_list", name="schedule_list"),
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
 

	
 
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"]
 
                save = True
 
            if "presentation" in form.cleaned_data:
 
                presentation = form.cleaned_data["presentation"]
 
                if presentation is None:
 
                    slot.unassign()
 
                else:
...
 
@@ -136,24 +135,24 @@ def schedule_slot_edit(request, slug, slot_pk):
 
        return redirect("schedule_edit", slug)
 
    else:
 
        form = SlotEditForm(slot=slot)
 
        ctx = {
 
            "slug": slug,
 
            "form": form,
 
            "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
 
from django.contrib.auth.models import User
 

	
 
from fixture_generator import fixture_generator
 

	
 
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
 
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",
 
        ]
 
        widgets = {
 
            "biography": MarkItUpWidget(),
 
        }
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
 
import datetime
 

	
 
from django.db import models
 
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)
 
            for p in self.copresentations.all():
 
                presentations.append(p)
 
        return presentations
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"),
 
    url(r"^edit/(?:(?P<pk>\d+)/)?$", "speaker_edit", name="speaker_edit"),
 
    url(r"^profile/(?P<pk>\d+)/$", "speaker_profile", name="speaker_profile"),
 
    url(r"^staff/create/(\d+)/$", "speaker_create_staff", name="speaker_create_staff"),
 
)
symposion/speakers/views.py
Show inline comments
...
 
@@ -8,129 +8,129 @@ from django.contrib.auth.models import User
 

	
 
from symposion.proposals.models import ProposalBase
 
from symposion.speakers.forms import SpeakerForm
 
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):
 
    speaker = get_object_or_404(Speaker, invite_token=token)
 
    request.session["pending-token"] = token
 
    if request.user.is_authenticated():
 
        # check for speaker profile
 
        try:
 
            existing_speaker = request.user.speaker_profile
 
        except ObjectDoesNotExist:
 
            pass
 
        else:
 
            del request.session["pending-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")
 

	
 

	
 
@login_required
 
def speaker_edit(request, pk=None):
 
    if pk is None:
 
        try:
 
            speaker = request.user.speaker_profile
 
        except Speaker.DoesNotExist:
 
            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
 

	
 

	
 
class SponsorBenefitInline(admin.StackedInline):
 
    model = SponsorBenefit
 
    extra = 0
 
    fieldsets = [
 
        (None, {
 
            "fields": [
 
                ("benefit", "active"),
 
                ("max_words", "other_limits"),
 
                "text",
 
                "upload",
 
            ]
 
        })
 
    ]
 

	
 

	
 
class SponsorAdmin(admin.ModelAdmin):
 
    
 

 
    save_on_top = True
 
    fieldsets = [
 
        (None, {
 
            "fields": [
 
                ("name", "applicant"),
 
                ("level", "active"),
 
                "external_url",
 
                "annotation",
 
                ("contact_name", "contact_email")
 
            ]
 
        }),
 
        ("Metadata", {
 
            "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
...
 
@@ -7,72 +7,72 @@ from symposion.sponsorship.models import Sponsor, SponsorBenefit
 

	
 

	
 
class SponsorApplicationForm(forms.ModelForm):
 
    def __init__(self, *args, **kwargs):
 
        self.user = kwargs.pop("user")
 
        kwargs.update({
 
            "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
 

	
 

	
 
class SponsorDetailsForm(forms.ModelForm):
 
    class Meta:
 
        model = Sponsor
 
        fields = [
 
            "name",
 
            "external_url",
 
            "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,
 
    can_delete=False, extra=0,
 
    fields=["text", "upload"]
 
)
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
...
 
@@ -5,132 +5,136 @@ from django.core.urlresolvers import reverse
 
from django.db import models
 
from django.db.models.signals import post_init, post_save
 
from django.utils.translation import ugettext_lazy as _
 

	
 
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:
 
        instance._initial_level_id = instance.level_id
 
post_init.connect(_store_initial_level, sender=Sponsor)
 

	
 

	
 
def _check_level_change(sender, instance, created, **kwargs):
 
    if instance and (created or instance.level_id != instance._initial_level_id):
...
 
@@ -138,75 +142,76 @@ def _check_level_change(sender, instance, created, **kwargs):
 
post_save.connect(_check_level_change, sender=Sponsor)
 

	
 

	
 
BENEFIT_TYPE_CHOICES = [
 
    ("text", "Text"),
 
    ("file", "File"),
 
    ("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":
 
            return ["upload"]
 
        elif self.benefit.type == "text":
 
            return ["text"]
 
        return []
symposion/sponsorship/templatetags/sponsorship_tags.py
Show inline comments
 
from django import template
 

	
 
from symposion.conference.models import current_conference
 
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""
 

	
 

	
 
@register.tag
 
def sponsors(parser, token):
 
    """
 
    {% sponsors as all_sponsors %}
 
    or
 
    {% sponsors "gold" as gold_sponsors %}
 
    """
 
    return SponsorsNode.handle_token(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"),
 
    url(r"^apply/$", "sponsor_apply", name="sponsor_apply"),
 
    url(r"^add/$", "sponsor_add", name="sponsor_add"),
 
    url(r"^(?P<pk>\d+)/$", "sponsor_detail", name="sponsor_detail"),
 
)
symposion/sponsorship/views.py
Show inline comments
 
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
 
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"]
 

	
 
admin.site.register(Membership, MembershipAdmin)
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:
 
            return set()
 
        if not hasattr(user_obj, "_team_perm_cache"):
 
            memberships = Team.objects.filter(
 
                Q(memberships__user=user_obj),
 
                Q(memberships__state="manager") | Q(memberships__state="member"),
 
            )
symposion/teams/forms.py
Show inline comments
 
from django import forms
 

	
 
from django.utils.html import escape
 
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)
 
            membership.update(state="member")
symposion/teams/models.py
Show inline comments
...
 
@@ -11,55 +11,56 @@ TEAM_ACCESS_CHOICES = [
 
    ("open", "open"),
 
    ("application", "by application"),
 
    ("invitation", "by invitation")
 
]
 

	
 

	
 
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"),
 
    ("invited", "invited"),
 
    ("declined", "declined"),
 
    ("rejected", "rejected"),
 
    ("member", "member"),
 
    ("manager", "manager"),
 
]
symposion/teams/templatetags/teams_tags.py
Show inline comments
 
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:
 
                teams.append(team)
 
            elif request.user.is_staff and state is None:
 
                teams.append(team)
 
        context[self.context_var] = teams
 
        return u""
 

	
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
 
from django.http import Http404, HttpResponseNotAllowed
 
from django.shortcuts import render, redirect, get_object_or_404
 

	
 
from django.contrib.auth.decorators import login_required
 
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:
 
        return True
 
    elif state == "invited":
 
        return True
 
    elif user.is_staff and state is None:
 
        return True
 
    else:
...
 
@@ -41,97 +41,97 @@ def can_apply(team, user):
 
    else:
 
        return False
 

	
 

	
 
def can_invite(team, user):
 
    state = team.get_state_for_user(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),
 
        "can_apply": can_apply(team, request.user),
 
    })
 

	
 

	
 
@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)
 
    else:
 
        return redirect("team_detail", slug=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:
 
        return redirect("team_detail", slug=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={
 
            "team": team,
 
            "user": request.user
 
        })
 
        messages.success(request, "Applied to join team.")
 
        return redirect("team_detail", slug=slug)
 
    else:
symposion/utils/mail.py
Show inline comments
 
from django.conf import settings
 
from django.core.mail import EmailMultiAlternatives
 
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
...
 
@@ -3,30 +3,30 @@ import random
 

	
 
from django.shortcuts import render, redirect
 

	
 
from django.contrib.auth.models import User
 
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)
 

	
 
    def generate_username(self, form):
 
        def random_username():
 
            h = hashlib.sha1(form.cleaned_data["email"]).hexdigest()[:25]
 
            # don't ask
 
            n = random.randint(1, (10 ** (5 - 1)) - 1)
0 comments (0 inline, 0 general)