diff --git a/symposion/boxes/authorization.py b/symposion/boxes/authorization.py index 011e215d30915b4509d5329536e65731bace22b6..e898b0cf422eee1495383f4ca5811292c47e550e 100644 --- a/symposion/boxes/authorization.py +++ b/symposion/boxes/authorization.py @@ -13,8 +13,8 @@ def default_can_edit(request, *args, **kwargs): 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) diff --git a/symposion/boxes/forms.py b/symposion/boxes/forms.py index b5ad3b32d7bea7b84cdec075f514edd3476cd230..7f881acce2e0405ddda430b39c73485fefedfcc2 100644 --- a/symposion/boxes/forms.py +++ b/symposion/boxes/forms.py @@ -4,7 +4,7 @@ from symposion.boxes.models import Box class BoxForm(forms.ModelForm): - + class Meta: model = Box fields = ["content"] diff --git a/symposion/boxes/models.py b/symposion/boxes/models.py index c83cadcb6b9d3bcfcaeff2411f2742a37d86cc80..f12e5c9511a1ae9f493823505c1c7451b5e27096 100644 --- a/symposion/boxes/models.py +++ b/symposion/boxes/models.py @@ -1,5 +1,3 @@ -import datetime - from django.db import models from django.contrib.auth.models import User @@ -10,16 +8,16 @@ 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" diff --git a/symposion/boxes/templatetags/boxes_tags.py b/symposion/boxes/templatetags/boxes_tags.py index e300fd3bc392046c6dc15f78e3b3a3e91cf3c77b..53765e567f76656992e4f032c8d4e1409b345f30 100644 --- a/symposion/boxes/templatetags/boxes_tags.py +++ b/symposion/boxes/templatetags/boxes_tags.py @@ -1,10 +1,5 @@ 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 @@ -16,22 +11,22 @@ 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, diff --git a/symposion/boxes/urls.py b/symposion/boxes/urls.py index dc57fe6bd7fe5147a39210669f116f9086cd5cc2..c0cf3fb13332e8fe37ce7f050d3e8b55e2f15acd 100644 --- a/symposion/boxes/urls.py +++ b/symposion/boxes/urls.py @@ -1,6 +1,7 @@ +# 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 +) diff --git a/symposion/boxes/views.py b/symposion/boxes/views.py index b314b07e9f40a6c0cac83ef4c467e5647d553045..1e93675d238dbf8d4d73ac192a26bc8998465a4d 100644 --- a/symposion/boxes/views.py +++ b/symposion/boxes/views.py @@ -7,7 +7,8 @@ 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": @@ -20,17 +21,17 @@ def get_auth_vars(request): @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(): diff --git a/symposion/cms/admin.py b/symposion/cms/admin.py index c45e4cc114b01bfdf9f6d78ca4dcfb2c7dcf648c..83b4c1767374a699ccda7d251850f26e7e546305 100644 --- a/symposion/cms/admin.py +++ b/symposion/cms/admin.py @@ -4,6 +4,7 @@ import reversion from .models import Page + class PageAdmin(reversion.VersionAdmin): pass diff --git a/symposion/cms/forms.py b/symposion/cms/forms.py index 149ba34a5b8676ae5ecc447d3e5bad1ba838ad18..5eecca31dfdbed13dc28af737ab7f70ca437cf7d 100644 --- a/symposion/cms/forms.py +++ b/symposion/cms/forms.py @@ -6,7 +6,7 @@ from .models import Page class PageForm(forms.ModelForm): - + class Meta: model = Page fields = ["title", "body", "path"] @@ -17,5 +17,5 @@ class PageForm(forms.ModelForm): class FileUploadForm(forms.Form): - + file = forms.FileField() diff --git a/symposion/cms/managers.py b/symposion/cms/managers.py index c8381ebd47f561fb33bb885988a64876077a7762..fcf4a4e1ad6dc7f53c8443635608ec0e6e0616d6 100644 --- a/symposion/cms/managers.py +++ b/symposion/cms/managers.py @@ -2,8 +2,9 @@ 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()) diff --git a/symposion/cms/models.py b/symposion/cms/models.py index 4acc174a655f1d127fe23ab00c9d8cac6f2c41f9..25c1caea7792f5ed8282a4e757442aea5c0812e1 100644 --- a/symposion/cms/models.py +++ b/symposion/cms/models.py @@ -18,12 +18,12 @@ 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() @@ -32,28 +32,29 @@ class Page(models.Model): 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) @@ -64,9 +65,9 @@ def generate_filename(instance, 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()]) diff --git a/symposion/cms/urls.py b/symposion/cms/urls.py index 3297ce53b69a3be359d133dde12f34245613809d..be16e2cd40d37fd1850535ee015130f0e986fdf4 100644 --- a/symposion/cms/urls.py +++ b/symposion/cms/urls.py @@ -1,3 +1,4 @@ +# flake8: noqa from django.conf.urls.defaults import url, patterns PAGE_RE = r"(([\w-]{1,})(/[\w-]{1,})*)/" diff --git a/symposion/cms/views.py b/symposion/cms/views.py index 4a4ee610e6f4722b68baee8f68e5a7cf1e542ba4..2f58381a077c219122d207515cd6c4d8e113bc5a 100644 --- a/symposion/cms/views.py +++ b/symposion/cms/views.py @@ -23,20 +23,20 @@ def can_upload(user): 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, @@ -45,15 +45,15 @@ def page(request, path): @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(): @@ -65,7 +65,7 @@ def page_edit(request, path): print form.errors else: form = PageForm(instance=page, initial={"path": path}) - + return render(request, "cms/page_edit.html", { "path": path, "form": form @@ -75,7 +75,7 @@ def page_edit(request, path): def file_index(request): if not can_upload(request.user): raise Http404 - + ctx = { "files": File.objects.all(), } @@ -85,7 +85,7 @@ def file_index(request): 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(): @@ -97,7 +97,7 @@ def file_create(request): return redirect("file_index") else: form = FileUploadForm() - + ctx = { "form": form, } @@ -106,7 +106,7 @@ def file_create(request): 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 @@ -115,14 +115,14 @@ def file_download(request, pk, *args): 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() diff --git a/symposion/conf.py b/symposion/conf.py index e2fb8072b613b812f51b1b5a7045976c1752dacb..dd2a6d5035bad4b8c1f13f834322835a17b4351e 100644 --- a/symposion/conf.py +++ b/symposion/conf.py @@ -1,8 +1,6 @@ -from django.conf import settings - from appconf import AppConf class SymposionAppConf(AppConf): - + VOTE_THRESHOLD = 3 diff --git a/symposion/conference/admin.py b/symposion/conference/admin.py index 7f0b55c21da382ce5f99e0b458ed7bfd730ea1bc..c8e57441b7da5e0dbc0b5602a8bc0a40d0fd5b43 100644 --- a/symposion/conference/admin.py +++ b/symposion/conference/admin.py @@ -6,6 +6,6 @@ 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") ) diff --git a/symposion/conference/models.py b/symposion/conference/models.py index 4bb6b8ce256d9787fd6b63c0afa4c01543a8b780..354403b67721257d53c9cb6b4cbec0d97ead383d 100644 --- a/symposion/conference/models.py +++ b/symposion/conference/models.py @@ -11,24 +11,24 @@ 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() @@ -36,7 +36,7 @@ class Conference(models.Model): del CONFERENCE_CACHE[pk] except KeyError: pass - + class Meta(object): verbose_name = _("conference") verbose_name_plural = _("conferences") @@ -48,19 +48,19 @@ class Section(models.Model): "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") diff --git a/symposion/conference/urls.py b/symposion/conference/urls.py index 6bd44c16c49fac3f86d32543e4a729fc35f15fb2..f2cdd52b55e521b4729dc41a0f16f7befb6de4b0 100644 --- a/symposion/conference/urls.py +++ b/symposion/conference/urls.py @@ -1,4 +1,5 @@ -from django.conf.urls.defaults import * +# flake8: noqa +from django.conf.urls.defaults import patterns, url urlpatterns = patterns("symposion.conference.views", diff --git a/symposion/conference/views.py b/symposion/conference/views.py index 7edf644a922d323290e71054617978e91da256d4..3be21e31c4c2e47f27da77ac3bfc8315b1b7106d 100644 --- a/symposion/conference/views.py +++ b/symposion/conference/views.py @@ -7,10 +7,10 @@ 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(), }) diff --git a/symposion/forms.py b/symposion/forms.py index 0a623606828b87b372f69562af7274c0fcf37f63..7799e06aef6193727445c915dd7e51e8235c4c07 100644 --- a/symposion/forms.py +++ b/symposion/forms.py @@ -4,7 +4,7 @@ import account.forms class SignupForm(account.forms.SignupForm): - + first_name = forms.CharField() last_name = forms.CharField() email_confirm = forms.EmailField(label="Confirm Email") @@ -20,11 +20,12 @@ class SignupForm(account.forms.SignupForm): "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 diff --git a/symposion/markdown_parser.py b/symposion/markdown_parser.py index 07216c2aa8aa40126f1670275c6fc0c548b6a55c..178708f655f9824bf2c6cfca7be4f0d82a3d9748 100644 --- a/symposion/markdown_parser.py +++ b/symposion/markdown_parser.py @@ -1,14 +1,13 @@ -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) diff --git a/symposion/proposals/actions.py b/symposion/proposals/actions.py index e46e252434505e0a85db4746273a17613628a563..3dcfb1681da164932de83b32a994b868955d759e 100644 --- a/symposion/proposals/actions.py +++ b/symposion/proposals/actions.py @@ -3,9 +3,8 @@ 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 @@ -24,12 +23,14 @@ def export_as_csv_action( 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 diff --git a/symposion/proposals/forms.py b/symposion/proposals/forms.py index 3f3fe115b4f009a52310d0fbe31c46c940191d9c..91c7e0042bc4ba5cd8a99af80e181d550ca4c347 100644 --- a/symposion/proposals/forms.py +++ b/symposion/proposals/forms.py @@ -9,15 +9,15 @@ from symposion.proposals.models import SupportingDocument 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( @@ -32,7 +32,7 @@ class AddSpeakerForm(forms.Form): class SupportingDocumentCreateForm(forms.ModelForm): - + class Meta: model = SupportingDocument fields = [ diff --git a/symposion/proposals/managers.py b/symposion/proposals/managers.py deleted file mode 100644 index b908c7b68edacd41b8bde56e20b4b8053b4430b3..0000000000000000000000000000000000000000 --- a/symposion/proposals/managers.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db import models -from django.db.models.query import QuerySet - - -class CachingM2MQuerySet(QuerySet): - - def __init__(self, *args, **kwargs): - super(CachingM2MQuerySet, self).__init__(*args, **kwargs) - self.cached_m2m_field = kwargs["m2m_field"] - - def iterator(self): - parent_iter = super(CachingM2MQuerySet, self).iterator() - m2m_model = getattr(self.model, self.cached_m2m_field).through - - for obj in parent_iter: - if obj.id in cached_objects: - setattr(obj, "_cached_m2m_%s" % self.cached_m2m_field) - yield obj - - -class ProposalManager(models.Manager): - def cache_m2m(self, m2m_field): - return CachingM2MQuerySet(self.model, using=self._db, m2m_field=m2m_field) - AdditionalSpeaker = queryset.model.additional_speakers.through - additional_speakers = collections.defaultdict(set) - for additional_speaker in AdditionalSpeaker._default_manager.filter(proposal__in=queryset).select_related("speaker__user"): - additional_speakers[additional_speaker.proposal_id].add(additional_speaker.speaker) \ No newline at end of file diff --git a/symposion/proposals/models.py b/symposion/proposals/models.py index e70af55035d5caef2d5362dde52cefdd7c19637b..c12c5bcc768ca32c43c075da355f6ddd3af18e4c 100644 --- a/symposion/proposals/models.py +++ b/symposion/proposals/models.py @@ -21,20 +21,20 @@ 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() @@ -43,7 +43,7 @@ class ProposalSection(models.Model): Q(end__gt=now) | Q(end=None), Q(closed=False) | Q(closed=None), ) - + def is_available(self): if self.closed: return False @@ -53,7 +53,7 @@ class ProposalSection(models.Model): if self.end and self.end < now: return False return True - + def __unicode__(self): return self.section.name @@ -61,68 +61,77 @@ class ProposalSection(models.Model): 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 Markdown.") + help_text=_("Detailed outline. Will be made public if your proposal is accepted. Edit " + "using Markdown.") ) 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 Markdown.") + 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 " + "Markdown.") ) 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, @@ -135,21 +144,21 @@ 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") @@ -162,14 +171,15 @@ def uuid_filename(instance, 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()]) diff --git a/symposion/proposals/templatetags/proposal_tags.py b/symposion/proposals/templatetags/proposal_tags.py index 2f728847e9d55a284999a9f0a032cd2f6ed7c651..f9581d3511ceb51747994f80ac9b2b373f0e8a50 100644 --- a/symposion/proposals/templatetags/proposal_tags.py +++ b/symposion/proposals/templatetags/proposal_tags.py @@ -7,7 +7,7 @@ register = template.Library() class AssociatedProposalsNode(template.Node): - + @classmethod def handle_token(cls, parser, token): bits = token.split_contents() @@ -15,10 +15,10 @@ class AssociatedProposalsNode(template.Node): 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: @@ -32,7 +32,7 @@ class AssociatedProposalsNode(template.Node): class PendingProposalsNode(template.Node): - + @classmethod def handle_token(cls, parser, token): bits = token.split_contents() @@ -40,10 +40,10 @@ class PendingProposalsNode(template.Node): 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: @@ -70,4 +70,3 @@ def associated_proposals(parser, token): {% associated_proposals as associated_proposals %} """ return AssociatedProposalsNode.handle_token(parser, token) - diff --git a/symposion/proposals/urls.py b/symposion/proposals/urls.py index 85e2bb1c5debb8401c38f8a2f3ca926244a939a2..c2efdcb538aa9ce1bbe5a1f15afd13c09ff4ed56 100644 --- a/symposion/proposals/urls.py +++ b/symposion/proposals/urls.py @@ -1,3 +1,4 @@ +# flake8: noqa from django.conf.urls.defaults import * @@ -11,7 +12,7 @@ urlpatterns = patterns("symposion.proposals.views", 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"), diff --git a/symposion/proposals/views.py b/symposion/proposals/views.py index cf532c2b5bc5c712db723b407f0206325ec19b4f..dea71b2005220d190e3aef854bf42a01f3cb9ab2 100644 --- a/symposion/proposals/views.py +++ b/symposion/proposals/views.py @@ -37,21 +37,21 @@ def proposal_submit(request): 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: @@ -59,12 +59,12 @@ def proposal_submit_kind(request, kind_slug): 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(): @@ -79,7 +79,7 @@ def proposal_submit_kind(request, kind_slug): return redirect("dashboard") else: form = form_class() - + return render(request, "proposals/proposal_submit_kind.html", { "kind": kind, "form": form, @@ -91,17 +91,17 @@ 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 @@ -135,13 +135,13 @@ def proposal_speaker_manage(request, pk): # 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) @@ -150,9 +150,10 @@ def proposal_speaker_manage(request, pk): # 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: @@ -173,14 +174,14 @@ def proposal_edit(request, 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": @@ -206,7 +207,7 @@ def proposal_edit(request, pk): return redirect("proposal_detail", proposal.pk) else: form = form_class(instance=proposal) - + return render(request, "proposals/proposal_edit.html", { "proposal": proposal, "form": form, @@ -218,22 +219,22 @@ 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( @@ -242,7 +243,7 @@ def proposal_detail(request, pk): user=request.user ).distinct().values_list("user", flat=True) ) - + for reviewer in reviewers: ctx = { "proposal": proposal, @@ -253,13 +254,13 @@ def proposal_detail(request, pk): [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 @@ -271,7 +272,7 @@ 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() @@ -281,7 +282,7 @@ def proposal_cancel(request, pk): # @@@ 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, }) @@ -311,7 +312,8 @@ def proposal_leave(request, pk): @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() @@ -324,7 +326,8 @@ def proposal_pending_join(request, pk): @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() @@ -339,10 +342,10 @@ 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(): @@ -353,7 +356,7 @@ def document_create(request, proposal_pk): return redirect("proposal_detail", proposal.pk) else: form = SupportingDocumentCreateForm() - + return render(request, "proposals/document_create.html", { "proposal": proposal, "form": form, @@ -378,8 +381,8 @@ def document_download(request, pk, *args): 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) diff --git a/symposion/reviews/context_processors.py b/symposion/reviews/context_processors.py index 6e70715b7ea3dc4dffc4835e2ecab07a15b25e0e..02850f15a791baa55b7d4a54618e0af010a3b4f1 100644 --- a/symposion/reviews/context_processors.py +++ b/symposion/reviews/context_processors.py @@ -1,5 +1,3 @@ -from django.contrib.contenttypes.models import ContentType - from symposion.proposals.models import ProposalSection diff --git a/symposion/reviews/forms.py b/symposion/reviews/forms.py index b9eee672e964d6c76d74fc71b78e4c2ce28168f0..a705333cad2cf5e2210c8efd5e34019d29438495 100644 --- a/symposion/reviews/forms.py +++ b/symposion/reviews/forms.py @@ -9,13 +9,13 @@ 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 ) @@ -23,14 +23,14 @@ 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): diff --git a/symposion/reviews/management/commands/assign_reviewers.py b/symposion/reviews/management/commands/assign_reviewers.py index 334eab3049bed783b7810114413b936285b84c32..20594f4cc8bc7cf355b80c76bfbfe8bccee5e78d 100644 --- a/symposion/reviews/management/commands/assign_reviewers.py +++ b/symposion/reviews/management/commands/assign_reviewers.py @@ -1,8 +1,3 @@ -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 diff --git a/symposion/reviews/management/commands/calculate_results.py b/symposion/reviews/management/commands/calculate_results.py index 45185a22551e41219699ac263a14a6d78128db2a..6a06ce8ac86497d1b8db15477ec48ad23f98260f 100644 --- a/symposion/reviews/management/commands/calculate_results.py +++ b/symposion/reviews/management/commands/calculate_results.py @@ -1,11 +1,9 @@ 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() diff --git a/symposion/reviews/management/commands/create_review_permissions.py b/symposion/reviews/management/commands/create_review_permissions.py index cde2c233cdaa019b08c09f8feda32dc593db2f3b..67dfc96b51590962e68e3fc3a20709a3fc79ef74 100644 --- a/symposion/reviews/management/commands/create_review_permissions.py +++ b/symposion/reviews/management/commands/create_review_permissions.py @@ -7,14 +7,14 @@ 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( diff --git a/symposion/reviews/management/commands/promoteproposals.py b/symposion/reviews/management/commands/promoteproposals.py index 72c6faabb04729eb1c065d14844c1440d7f02ba7..e62c6903f7a9e9b370badb0edc8b20c34ce22409 100644 --- a/symposion/reviews/management/commands/promoteproposals.py +++ b/symposion/reviews/management/commands/promoteproposals.py @@ -5,11 +5,12 @@ 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))") diff --git a/symposion/reviews/models.py b/symposion/reviews/models.py index 64d6db8d93ded2690b2874d33423c343e50a7e45..b601e0c283d475e9dc6c4165d8a6d09533280cc9 100644 --- a/symposion/reviews/models.py +++ b/symposion/reviews/models.py @@ -15,11 +15,11 @@ 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 @@ -29,7 +29,7 @@ class Votes(object): 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."), @@ -45,21 +45,21 @@ class ReviewAssignment(models.Model): 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()) @@ -94,7 +94,7 @@ class ReviewAssignment(models.Model): class ProposalMessage(models.Model): proposal = models.ForeignKey("proposals.ProposalBase", related_name="messages") user = models.ForeignKey(User) - + message = MarkupField() submitted_at = models.DateTimeField(default=datetime.now, editable=False) @@ -104,24 +104,24 @@ class ProposalMessage(models.Model): 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: @@ -130,7 +130,7 @@ class Review(models.Model): 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( @@ -152,7 +152,8 @@ class Review(models.Model): 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( @@ -166,7 +167,7 @@ class Review(models.Model): 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", @@ -174,7 +175,7 @@ class Review(models.Model): self.VOTES.MINUS_ZERO: "minus-zero", self.VOTES.MINUS_ONE: "minus-one", }[self.vote] - + @property def section(self): return self.proposal.kind.section.slug @@ -182,18 +183,18 @@ class Review(models.Model): 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", @@ -223,7 +224,7 @@ class ProposalResult(models.Model): ("undecided", "undecided"), ("standby", "standby"), ], default="undecided") - + @classmethod def full_calculate(cls): for proposal in ProposalBase.objects.all(): @@ -231,24 +232,24 @@ class ProposalResult(models.Model): 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", @@ -283,7 +284,7 @@ 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"), @@ -293,7 +294,7 @@ class Comment(models.Model): class NotificationTemplate(models.Model): - + label = models.CharField(max_length=100) from_address = models.EmailField() subject = models.CharField(max_length=100) @@ -301,15 +302,16 @@ class NotificationTemplate(models.Model): 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]) @@ -321,18 +323,18 @@ def promote_proposal(proposal): 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 diff --git a/symposion/reviews/templatetags/review_tags.py b/symposion/reviews/templatetags/review_tags.py index cfb6437e289f4036fe0daa482cc1d65e5a0ad9a5..c646ca962e0005a4af7733b4e6900777e006d194 100644 --- a/symposion/reviews/templatetags/review_tags.py +++ b/symposion/reviews/templatetags/review_tags.py @@ -1,6 +1,6 @@ from django import template -from symposion.reviews.models import Review, ReviewAssignment +from symposion.reviews.models import ReviewAssignment register = template.Library() diff --git a/symposion/reviews/tests.py b/symposion/reviews/tests.py index e5fe7cafdee703dcd759f14634e607c964cce53c..07e3cc995d707470fd7798a841c39974e8dab039 100644 --- a/symposion/reviews/tests.py +++ b/symposion/reviews/tests.py @@ -15,44 +15,44 @@ class login(object): 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")) @@ -60,10 +60,10 @@ class ReviewTests(TestCase): 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", @@ -72,7 +72,7 @@ class ReviewTests(TestCase): 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")) @@ -90,7 +90,7 @@ class ReviewTests(TestCase): 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", @@ -100,21 +100,21 @@ class ReviewTests(TestCase): 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.", }) @@ -122,7 +122,7 @@ class ReviewTests(TestCase): 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")) @@ -130,13 +130,13 @@ class ReviewTests(TestCase): 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. diff --git a/symposion/reviews/urls.py b/symposion/reviews/urls.py index 564bd35ebe68774750475716d89e1215c525d3bc..3cdc6f737580b55487f228a4162e8ef597a7957c 100644 --- a/symposion/reviews/urls.py +++ b/symposion/reviews/urls.py @@ -1,3 +1,4 @@ +# flake8: noqa from django.conf.urls.defaults import patterns, url @@ -14,9 +15,9 @@ urlpatterns = patterns("symposion.reviews.views", url(r"^section/(?P[\w\-]+)/notification/(?P\w+)/$", "result_notification", name="result_notification"), url(r"^section/(?P[\w\-]+)/notification/(?P\w+)/prepare/$", "result_notification_prepare", name="result_notification_prepare"), url(r"^section/(?P[\w\-]+)/notification/(?P\w+)/send/$", "result_notification_send", name="result_notification_send"), - + url(r"^review/(?P\d+)/$", "review_detail", name="review_detail"), - + url(r"^(?P\d+)/delete/$", "review_delete", name="review_delete"), url(r"^assignments/$", "review_assignments", name="review_assignments"), url(r"^assignment/(?P\d+)/opt-out/$", "review_assignment_opt_out", name="review_assignment_opt_out"), diff --git a/symposion/reviews/utils.py b/symposion/reviews/utils.py index b91e8166b8a3ef961f09f69f9359d2da24a701e5..80e87e496fcc9c2cf7d219a05245896087359f14 100644 --- a/symposion/reviews/utils.py +++ b/symposion/reviews/utils.py @@ -2,16 +2,16 @@ 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(): diff --git a/symposion/reviews/views.py b/symposion/reviews/views.py index 880e9e8b4786f8285255c5b4ff06cfbc66121a16..2be745e5234fbfbce58574cf6f3317bae4f9ae72 100644 --- a/symposion/reviews/views.py +++ b/symposion/reviews/views.py @@ -1,5 +1,3 @@ -import re - from django.core.mail import send_mass_mail from django.db.models import Q from django.http import HttpResponseBadRequest, HttpResponseNotAllowed @@ -27,18 +25,18 @@ def access_not_permitted(request): 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 @@ -46,37 +44,38 @@ def proposals_generator(request, queryset, user_pk=None, check_speaker=True): 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": @@ -88,35 +87,36 @@ def review_section(request, section_slug, assigned=False, reviewed="all"): 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, } @@ -125,41 +125,41 @@ def review_list(request, section_slug, user_pk): @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(), @@ -169,50 +169,50 @@ def review_admin(request, section_slug): @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 = { @@ -222,9 +222,9 @@ def review_detail(request, pk): } send_email( [speaker.email], "proposal_new_message", - context = ctx + context=ctx ) - + return redirect(request.path) else: initial = {} @@ -237,7 +237,7 @@ def review_detail(request, pk): elif "result_submit" in request.POST: if admin: result = request.POST["result_submit"] - + if result == "accept": proposal.result.status = "accepted" proposal.result.save() @@ -250,7 +250,7 @@ def review_detail(request, pk): elif result == "standby": proposal.result.status = "standby" proposal.result.save() - + return redirect(request.path) else: initial = {} @@ -261,17 +261,17 @@ def review_detail(request, pk): 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, @@ -287,53 +287,64 @@ def review_detail(request, pk): 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, @@ -341,7 +352,7 @@ def review_status(request, section_slug=None, key=None): }) else: ctx["proposals"] = proposals - + return render(request, "reviews/review_stats.html", ctx) @@ -361,14 +372,13 @@ def review_assignments(request): @login_required @require_POST def review_assignment_opt_out(request, pk): - review_assignment = get_object_or_404(ReviewAssignment, - pk=pk, - user=request.user - ) + review_assignment = get_object_or_404( + ReviewAssignment, pk=pk, user=request.user) if not review_assignment.opted_out: review_assignment.opted_out = True review_assignment.save() - ReviewAssignment.create_assignments(review_assignment.proposal, origin=ReviewAssignment.AUTO_ASSIGNED_LATER) + ReviewAssignment.create_assignments( + review_assignment.proposal, origin=ReviewAssignment.AUTO_ASSIGNED_LATER) return redirect("review_assignments") @@ -387,7 +397,7 @@ def review_bulk_accept(request, section_slug): return redirect("review_section", section_slug=section_slug) else: form = BulkPresentationForm() - + return render(request, "reviews/review_bulk_accept.html", { "form": form, }) @@ -397,10 +407,12 @@ def review_bulk_accept(request, section_slug): 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, @@ -414,10 +426,10 @@ def result_notification(request, section_slug, status): 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"): @@ -431,13 +443,13 @@ def result_notification_prepare(request, section_slug, 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, @@ -452,18 +464,18 @@ def result_notification_prepare(request, section_slug, status): 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, @@ -471,15 +483,15 @@ def result_notification_send(request, section_slug, 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 @@ -494,7 +506,7 @@ def result_notification_send(request, section_slug, status): ) rn.save() emails.append(rn.email_args) - + send_mass_mail(emails) - + return redirect("result_notification", section_slug=section_slug, status=status) diff --git a/symposion/schedule/forms.py b/symposion/schedule/forms.py index 63316743529a7f34a5c7419c2cc1176ba0016402..5a0f97c1312932aadfcf8160085a8d0044637ae5 100644 --- a/symposion/schedule/forms.py +++ b/symposion/schedule/forms.py @@ -7,7 +7,7 @@ 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) @@ -16,7 +16,7 @@ class SlotEditForm(forms.Form): 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() @@ -31,7 +31,7 @@ class SlotEditForm(forms.Form): kwargs["required"] = True kwargs["queryset"] = queryset return forms.ModelChoiceField(**kwargs) - + def build_content_override_field(self): kwargs = { "label": "Content", diff --git a/symposion/schedule/models.py b/symposion/schedule/models.py index dc070917d0c4129abf55807649f4906ed847b330..0d7f642e9b47b3f763334247a3f767c5cd288f8b 100644 --- a/symposion/schedule/models.py +++ b/symposion/schedule/models.py @@ -2,44 +2,43 @@ 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 @@ -49,22 +48,22 @@ 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 @@ -73,7 +72,7 @@ class Slot(models.Model): self.unassign() content.slot = self content.save() - + def unassign(self): """ Unassign the associated content with this slot. @@ -81,7 +80,7 @@ class Slot(models.Model): if self.content and self.content.slot_id: self.content.slot = None self.content.save() - + @property def content(self): """ @@ -92,14 +91,14 @@ class Slot(models.Model): 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"] @@ -108,48 +107,49 @@ 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"] diff --git a/symposion/schedule/timetable.py b/symposion/schedule/timetable.py index 2aba1de30ce972995f0a6a2d3f78103719962444..1756db6c9ea0fe65799769239a36f777c2392ba5 100644 --- a/symposion/schedule/timetable.py +++ b/symposion/schedule/timetable.py @@ -1,5 +1,4 @@ import itertools -import operator from django.db.models import Count, Min @@ -7,22 +6,23 @@ 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")) @@ -38,7 +38,7 @@ class TimeTable(object): 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) diff --git a/symposion/schedule/urls.py b/symposion/schedule/urls.py index 58890827b1f63a0be3d9c3c423789ae769b0875b..59e9b55f72846083dd0bd70d3398f1a02541733b 100644 --- a/symposion/schedule/urls.py +++ b/symposion/schedule/urls.py @@ -1,3 +1,4 @@ +# flake8: noqa from django.conf.urls.defaults import url, patterns diff --git a/symposion/schedule/views.py b/symposion/schedule/views.py index 35f9e4326c24acbd4d1a3a0c4b8bec988475681e..98771b597088f9913392131f2ee44d182f0ba16e 100644 --- a/symposion/schedule/views.py +++ b/symposion/schedule/views.py @@ -1,4 +1,3 @@ -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 @@ -12,7 +11,7 @@ from symposion.schedule.timetable import TimeTable def fetch_schedule(slug): qs = Schedule.objects.all() - + if slug is None: if qs.count() > 1: raise Http404() @@ -21,14 +20,14 @@ def fetch_schedule(slug): 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) @@ -37,7 +36,7 @@ def schedule_conference(request): "schedule": schedule, "days": days, }) - + ctx = { "sections": sections, } @@ -45,14 +44,14 @@ def schedule_conference(request): 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, @@ -62,10 +61,10 @@ def schedule_detail(request, slug=None): 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, @@ -75,32 +74,32 @@ def schedule_list(request, slug=None): 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 = { @@ -112,12 +111,12 @@ def schedule_edit(request, slug=None): @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(): @@ -145,13 +144,13 @@ def schedule_slot_edit(request, slug, slot_pk): 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, diff --git a/symposion/speakers/admin.py b/symposion/speakers/admin.py index 8162ac3f9d17b8a1dce1552471f5a7eb11dc861a..479c962608b9171d789a688fa2ca80d43bc1219c 100644 --- a/symposion/speakers/admin.py +++ b/symposion/speakers/admin.py @@ -4,6 +4,5 @@ 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"]) diff --git a/symposion/speakers/fixture_gen.py b/symposion/speakers/fixture_gen.py index fc38463f254c6cbf0cdf004e14a304b4d092a58a..fdecb6b73ff8fa4a7932d78dea34aa33b209c0c0 100644 --- a/symposion/speakers/fixture_gen.py +++ b/symposion/speakers/fixture_gen.py @@ -10,7 +10,7 @@ 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", @@ -19,8 +19,8 @@ def speakers(): 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, diff --git a/symposion/speakers/forms.py b/symposion/speakers/forms.py index 334dcea2f2d0d74c61d77db476765f533658b561..14ecb9a9a9f47f9a0a0df89f45dbaddc0ca9954d 100644 --- a/symposion/speakers/forms.py +++ b/symposion/speakers/forms.py @@ -6,7 +6,7 @@ from symposion.speakers.models import Speaker class SpeakerForm(forms.ModelForm): - + class Meta: model = Speaker fields = [ diff --git a/symposion/speakers/management/commands/export_speaker_data.py b/symposion/speakers/management/commands/export_speaker_data.py index 5f9f565d89b5a9dc48017e11432076c33f4dc58e..00e82588e3c3806dc9320556868d79096138ac58 100644 --- a/symposion/speakers/management/commands/export_speaker_data.py +++ b/symposion/speakers/management/commands/export_speaker_data.py @@ -1,17 +1,17 @@ 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"), diff --git a/symposion/speakers/models.py b/symposion/speakers/models.py index c86c9aa21cd32c15a6c466878bd56abd10fca7f3..b5705bcc95e5e2893f44dacfd4c247410d0a54c6 100644 --- a/symposion/speakers/models.py +++ b/symposion/speakers/models.py @@ -9,44 +9,47 @@ 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 Markdown.") + 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 " + "" + "Markdown.")) 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 = [] diff --git a/symposion/speakers/urls.py b/symposion/speakers/urls.py index fa7055cf5748e6a706bc4b44a9e3b7e2faa11d07..0d1c7b1304d9958c3a24d8ba2632e99ea8c32f16 100644 --- a/symposion/speakers/urls.py +++ b/symposion/speakers/urls.py @@ -1,3 +1,4 @@ +# flake8: noqa from django.conf.urls.defaults import * diff --git a/symposion/speakers/views.py b/symposion/speakers/views.py index 17eb13701312d4bd96ca2fe742cfdab5f084cb30..fbd4c57c5a3b4482cb15d1e23514981615ad6dc7 100644 --- a/symposion/speakers/views.py +++ b/symposion/speakers/views.py @@ -17,7 +17,7 @@ def speaker_create(request): return redirect(request.user.speaker_profile) except ObjectDoesNotExist: pass - + if request.method == "POST": try: speaker = Speaker.objects.get(invite_email=request.user.email) @@ -26,7 +26,7 @@ def speaker_create(request): 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 @@ -37,7 +37,7 @@ def speaker_create(request): return redirect("dashboard") else: form = SpeakerForm(initial={"name": request.user.get_full_name()}) - + return render(request, "speakers/speaker_create.html", { "form": form, }) @@ -48,15 +48,15 @@ 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 @@ -65,7 +65,7 @@ def speaker_create_staff(request, pk): return redirect("user_list") else: form = SpeakerForm(initial={"name": user.get_full_name()}) - + return render(request, "speakers/speaker_create.html", { "form": form, }) @@ -88,8 +88,8 @@ def speaker_create_token(request, token): ).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(): @@ -109,7 +109,7 @@ def speaker_edit(request, pk=None): 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(): @@ -118,7 +118,7 @@ def speaker_edit(request, pk=None): return redirect("dashboard") else: form = SpeakerForm(instance=speaker) - + return render(request, "speakers/speaker_edit.html", { "form": form, }) @@ -129,7 +129,7 @@ def speaker_profile(request, 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, diff --git a/symposion/sponsorship/admin.py b/symposion/sponsorship/admin.py index ad39a62676f5333d53b48f278591e8c18a8d0952..b64c1a132169214e187eec8e80eb625153a476b1 100644 --- a/symposion/sponsorship/admin.py +++ b/symposion/sponsorship/admin.py @@ -1,6 +1,7 @@ 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): @@ -24,7 +25,7 @@ class SponsorBenefitInline(admin.StackedInline): class SponsorAdmin(admin.ModelAdmin): - + save_on_top = True fieldsets = [ (None, { @@ -43,7 +44,7 @@ class SponsorAdmin(admin.ModelAdmin): ] 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) @@ -56,13 +57,13 @@ class SponsorAdmin(admin.ModelAdmin): class BenefitAdmin(admin.ModelAdmin): - + list_display = ["name", "type", "description"] inlines = [BenefitLevelInline] class SponsorLevelAdmin(admin.ModelAdmin): - + inlines = [BenefitLevelInline] diff --git a/symposion/sponsorship/forms.py b/symposion/sponsorship/forms.py index 9584c7fb4f9c913dc81b4b92a39daeb4564fa7ad..33b52f422371802b9254aeb6ee9a910cbbfd9a4f 100644 --- a/symposion/sponsorship/forms.py +++ b/symposion/sponsorship/forms.py @@ -16,7 +16,7 @@ class SponsorApplicationForm(forms.ModelForm): } }) super(SponsorApplicationForm, self).__init__(*args, **kwargs) - + class Meta: model = Sponsor fields = [ @@ -26,7 +26,7 @@ class SponsorApplicationForm(forms.ModelForm): "contact_email", "level" ] - + def save(self, commit=True): obj = super(SponsorApplicationForm, self).save(commit=False) obj.applicant = self.user @@ -47,26 +47,26 @@ class SponsorDetailsForm(forms.ModelForm): 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 diff --git a/symposion/sponsorship/management/commands/reset_sponsor_benefits.py b/symposion/sponsorship/management/commands/reset_sponsor_benefits.py index 2c515a2e0989ab6c41da646eb705489a5b60a072..3c17efab949d8f93baa4cd3ddf148223aebf2b56 100644 --- a/symposion/sponsorship/management/commands/reset_sponsor_benefits.py +++ b/symposion/sponsorship/management/commands/reset_sponsor_benefits.py @@ -1,15 +1,13 @@ 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: @@ -19,17 +17,17 @@ class Command(BaseCommand): # 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, diff --git a/symposion/sponsorship/models.py b/symposion/sponsorship/models.py index 24a12d31728397c86e83ebd261b0e708d81d1b4e..e29ac3d11c8614dceea2719935631cfeff342573 100644 --- a/symposion/sponsorship/models.py +++ b/symposion/sponsorship/models.py @@ -14,29 +14,30 @@ 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) @@ -45,34 +46,36 @@ class Sponsor(models.Model): 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"): @@ -82,46 +85,47 @@ class Sponsor(models.Model): 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? @@ -147,59 +151,60 @@ BENEFIT_TYPE_CHOICES = [ 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 diff --git a/symposion/sponsorship/templatetags/sponsorship_tags.py b/symposion/sponsorship/templatetags/sponsorship_tags.py index cd1f33ba025d1338402d0f9efff0ca2b09edca7a..d88243c4e846f744d80f196a272a1baf877d0afc 100644 --- a/symposion/sponsorship/templatetags/sponsorship_tags.py +++ b/symposion/sponsorship/templatetags/sponsorship_tags.py @@ -8,7 +8,7 @@ register = template.Library() class SponsorsNode(template.Node): - + @classmethod def handle_token(cls, parser, token): bits = token.split_contents() @@ -18,27 +18,30 @@ class SponsorsNode(template.Node): 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() @@ -46,10 +49,10 @@ class SponsorLevelNode(template.Node): 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) @@ -72,4 +75,3 @@ def sponsor_levels(parser, token): {% sponsor_levels as levels %} """ return SponsorLevelNode.handle_token(parser, token) - \ No newline at end of file diff --git a/symposion/sponsorship/urls.py b/symposion/sponsorship/urls.py index e5d32bb0bec1266f84be261e9becc04bd72b9fdd..e6ba5fa3e99377b39d36133a41ad1ad0c31b822b 100644 --- a/symposion/sponsorship/urls.py +++ b/symposion/sponsorship/urls.py @@ -1,3 +1,4 @@ +# flake8: noqa from django.conf.urls.defaults import patterns, url from django.views.generic.simple import direct_to_template diff --git a/symposion/sponsorship/views.py b/symposion/sponsorship/views.py index 22b4f2433a9ed68a62141624c7c1b8ea5259bae6..c7728aedecaf44784b5bc657fbc9f73175ceafbc 100644 --- a/symposion/sponsorship/views.py +++ b/symposion/sponsorship/views.py @@ -5,7 +5,8 @@ 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 @@ -18,7 +19,7 @@ def sponsor_apply(request): 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)) @@ -28,7 +29,7 @@ def sponsor_apply(request): 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(): @@ -38,7 +39,7 @@ def sponsor_add(request): 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)) @@ -47,31 +48,31 @@ def sponsor_add(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, diff --git a/symposion/teams/admin.py b/symposion/teams/admin.py index 14b9ec25aa9f1d9c1871800cff5076ed0f1bd62a..5175e302a213f0f62f16ad197a8aeb8c48ec13af 100644 --- a/symposion/teams/admin.py +++ b/symposion/teams/admin.py @@ -5,8 +5,7 @@ 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): diff --git a/symposion/teams/backends.py b/symposion/teams/backends.py index 9b4ddf7efde8a1c77a21d3340f1f468efe9a6141..23b001b6e30e2456df4130094bf9177b51378d2b 100644 --- a/symposion/teams/backends.py +++ b/symposion/teams/backends.py @@ -4,10 +4,10 @@ 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 diff --git a/symposion/teams/forms.py b/symposion/teams/forms.py index 6004ae217f0b5444f69890a73656af10d5a77216..a9080626b7e9d24b755a5f48f6fd29bf028598c2 100644 --- a/symposion/teams/forms.py +++ b/symposion/teams/forms.py @@ -9,40 +9,43 @@ 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 %s found on this conference site" % escape(email))) - + raise forms.ValidationError( + mark_safe("no account with email address %s 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") diff --git a/symposion/teams/models.py b/symposion/teams/models.py index 7f00f05514f519b998885a3a0c37dbdda7d69281..8c344cc419f8539e2436821d6228f0624f2278e8 100644 --- a/symposion/teams/models.py +++ b/symposion/teams/models.py @@ -20,19 +20,20 @@ class Team(models.Model): 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 @@ -41,16 +42,16 @@ class Team(models.Model): 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") diff --git a/symposion/teams/templatetags/teams_tags.py b/symposion/teams/templatetags/teams_tags.py index 7c9cd5b574d78607ea08fcec7cc973bd61a7b434..b82a0eba2a0dd186e17e2d0546a10b63e412e49d 100644 --- a/symposion/teams/templatetags/teams_tags.py +++ b/symposion/teams/templatetags/teams_tags.py @@ -6,7 +6,7 @@ register = template.Library() class AvailableTeamsNode(template.Node): - + @classmethod def handle_token(cls, parser, token): bits = token.split_contents() @@ -14,10 +14,10 @@ class AvailableTeamsNode(template.Node): 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 = [] diff --git a/symposion/teams/urls.py b/symposion/teams/urls.py index 01145f41c8b1536087ed3199ae45aad6b3dccfeb..d70a600d40aadb5b83e4ded873f2fdb5534ba8f3 100644 --- a/symposion/teams/urls.py +++ b/symposion/teams/urls.py @@ -1,3 +1,4 @@ +# flake8: noqa from django.conf.urls.defaults import * @@ -7,7 +8,7 @@ urlpatterns = patterns("symposion.teams.views", url(r"^(?P[\w\-]+)/join/$", "team_join", name="team_join"), url(r"^(?P[\w\-]+)/leave/$", "team_leave", name="team_leave"), url(r"^(?P[\w\-]+)/apply/$", "team_apply", name="team_apply"), - + # membership specific url(r"^promote/(?P\d+)/$", "team_promote", name="team_promote"), url(r"^demote/(?P\d+)/$", "team_demote", name="team_demote"), diff --git a/symposion/teams/views.py b/symposion/teams/views.py index e885f9b4fc60aeb544676a0ed221827b68152640..f45a0cba764efcb54ae959223429da691ad882f7 100644 --- a/symposion/teams/views.py +++ b/symposion/teams/views.py @@ -10,7 +10,7 @@ from symposion.teams.forms import TeamInvitationForm from symposion.teams.models import Team, Membership -## perm checks +# perm checks # # @@@ these can be moved @@ -50,7 +50,7 @@ def can_invite(team, user): return False -## views +# views @login_required @@ -59,7 +59,7 @@ def team_detail(request, 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) @@ -72,7 +72,7 @@ def team_detail(request, slug): form = TeamInvitationForm(team=team) else: form = None - + return render(request, "teams/team_detail.html", { "team": team, "state": state, @@ -89,7 +89,7 @@ def team_join(request, 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" @@ -106,7 +106,7 @@ def team_leave(request, 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() @@ -122,7 +122,7 @@ def team_apply(request, 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" diff --git a/symposion/utils/mail.py b/symposion/utils/mail.py index dc85687178da8d70bff0b0706a35ce87194e7642..59cdf09c7281efaf3ed9136aa0e82d1dfd40b093 100644 --- a/symposion/utils/mail.py +++ b/symposion/utils/mail.py @@ -7,9 +7,9 @@ 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, @@ -19,12 +19,12 @@ def send_email(to, kind, **kwargs): 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() diff --git a/symposion/views.py b/symposion/views.py index 669e2847adae96c34a4ea63bbb2d1d393d5c202a..e1bfab7ee71dfaaa44d0b6768e20c64c5fd5e18f 100644 --- a/symposion/views.py +++ b/symposion/views.py @@ -12,12 +12,12 @@ 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"],