diff --git a/pycon/__init__.py b/pycon/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pycon/admin.py b/pycon/admin.py new file mode 100644 index 00000000..66933a2e --- /dev/null +++ b/pycon/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from pycon.models import PyConProposalCategory, PyConTalkProposal, PyConTutorialProposal, PyConPosterProposal + +admin.site.register(PyConProposalCategory) +admin.site.register(PyConTalkProposal) +admin.site.register(PyConTutorialProposal) +admin.site.register(PyConPosterProposal) \ No newline at end of file diff --git a/pycon/forms.py b/pycon/forms.py new file mode 100644 index 00000000..74c1b0b3 --- /dev/null +++ b/pycon/forms.py @@ -0,0 +1,84 @@ +from django import forms + +from markitup.widgets import MarkItUpWidget + +from pycon.models import PyConProposalCategory, PyConTalkProposal, PyConTutorialProposal, PyConPosterProposal + + +class PyConProposalForm(forms.ModelForm): + + def __init__(self, *args, **kwargs): + super(PyConProposalForm, self).__init__(*args, **kwargs) + self.fields["category"] = forms.ModelChoiceField( + queryset = PyConProposalCategory.objects.order_by("name") + ) + + def clean_description(self): + value = self.cleaned_data["description"] + if len(value) > 400: + raise forms.ValidationError( + u"The description must be less than 400 characters" + ) + return value + + +class PyConTalkProposalForm(PyConProposalForm): + + class Meta: + model = PyConTalkProposal + fields = [ + "title", + "category", + "audience_level", + "extreme", + "duration", + "description", + "abstract", + "additional_notes", + "recording_release", + ] + widgets = { + "abstract": MarkItUpWidget(), + "additional_notes": MarkItUpWidget(), + } + + +class PyConTutorialProposalForm(PyConProposalForm): + + class Meta: + model = PyConTutorialProposal + fields = [ + "title", + "category", + "audience_level", + "description", + "abstract", + "additional_notes", + "recording_release", + + ] + widgets = { + "abstract": MarkItUpWidget(), + "additional_notes": MarkItUpWidget(), + } + + +class PyConPosterProposalForm(PyConProposalForm): + + class Meta: + model = PyConPosterProposal + fields = [ + "title", + "category", + "audience_level", + "description", + "abstract", + "additional_notes", + "recording_release", + + ] + widgets = { + "abstract": MarkItUpWidget(), + "additional_notes": MarkItUpWidget(), + } + diff --git a/pycon/models.py b/pycon/models.py new file mode 100644 index 00000000..9e2299ea --- /dev/null +++ b/pycon/models.py @@ -0,0 +1,68 @@ +from django.db import models + +from symposion.proposals.models import ProposalBase + + +class PyConProposalCategory(models.Model): + + name = models.CharField(max_length=100) + slug = models.SlugField() + + def __unicode__(self): + return self.name + + class Meta: + verbose_name = "PyCon proposal category" + verbose_name_plural = "PyCon proposal categories" + + +class PyConProposal(ProposalBase): + + AUDIENCE_LEVEL_NOVICE = 1 + AUDIENCE_LEVEL_EXPERIENCED = 2 + AUDIENCE_LEVEL_INTERMEDIATE = 3 + + AUDIENCE_LEVELS = [ + (AUDIENCE_LEVEL_NOVICE, "Novice"), + (AUDIENCE_LEVEL_INTERMEDIATE, "Intermediate"), + (AUDIENCE_LEVEL_EXPERIENCED, "Experienced"), + ] + + category = models.ForeignKey(PyConProposalCategory) + audience_level = models.IntegerField(choices=AUDIENCE_LEVELS) + + recording_release = models.BooleanField( + default=True, + help_text="By submitting your talk proposal, you agree to give permission to the Python Software Foundation to record, edit, and release audio and/or video of your presentation. If you do not agree to this, please uncheck this box. See PyCon 2013 Recording Release for details." + ) + + class Meta: + abstract = True + + +class PyConTalkProposal(PyConProposal): + + DURATION_CHOICES = [ + (0, "No preference"), + (1, "I prefer a 30 minute slot"), + (2, "I prefer a 45 minute slot"), + ] + + extreme = models.BooleanField( + default=False, + help_text="'Extreme' talks are advanced talks with little or no introductory material. See Extreme Talks for details." + ) + duration = models.IntegerField(choices=DURATION_CHOICES) + + class Meta: + verbose_name = "PyCon talk proposal" + + +class PyConTutorialProposal(PyConProposal): + class Meta: + verbose_name = "PyCon tutorial proposal" + + +class PyConPosterProposal(PyConProposal): + class Meta: + verbose_name = "PyCon poster proposal" diff --git a/pycon/sponsorship/__init__.py b/pycon/sponsorship/__init__.py new file mode 100644 index 00000000..3cb4bc97 --- /dev/null +++ b/pycon/sponsorship/__init__.py @@ -0,0 +1,5 @@ +SPONSOR_COORDINATORS = "sponsor-coordinators" + +AUTH_GROUPS = [ + SPONSOR_COORDINATORS +] diff --git a/pycon/sponsorship/admin.py b/pycon/sponsorship/admin.py new file mode 100644 index 00000000..22096c77 --- /dev/null +++ b/pycon/sponsorship/admin.py @@ -0,0 +1,69 @@ +from django.contrib import admin + +from pycon.sponsorship.models import SponsorLevel, Sponsor, Benefit, BenefitLevel, SponsorBenefit + + +class BenefitLevelInline(admin.TabularInline): + model = BenefitLevel + extra = 0 + + +class SponsorBenefitInline(admin.StackedInline): + model = SponsorBenefit + extra = 0 + fieldsets = [ + (None, { + "fields": [ + ("benefit", "active"), + ("max_words", "other_limits"), + "text", + "upload", + ] + }) + ] + + +class SponsorAdmin(admin.ModelAdmin): + + save_on_top = True + fieldsets = [ + (None, { + "fields": [ + ("name", "applicant"), + ("level", "active"), + "external_url", + "annotation", + ("contact_name", "contact_email") + ] + }), + ("Metadata", { + "fields": ["added"], + "classes": ["collapse"] + }) + ] + inlines = [SponsorBenefitInline] + + def get_form(self, *args, **kwargs): + # @@@ kinda ugly but using choices= on NullBooleanField is broken + form = super(SponsorAdmin, self).get_form(*args, **kwargs) + form.base_fields["active"].widget.choices = [ + (u"1", "unreviewed"), + (u"2", "approved"), + (u"3", "rejected") + ] + return form + + +class BenefitAdmin(admin.ModelAdmin): + + inlines = [BenefitLevelInline] + + +class SponsorLevelAdmin(admin.ModelAdmin): + + inlines = [BenefitLevelInline] + + +admin.site.register(SponsorLevel, SponsorLevelAdmin) +admin.site.register(Sponsor, SponsorAdmin) +admin.site.register(Benefit, BenefitAdmin) \ No newline at end of file diff --git a/pycon/sponsorship/forms.py b/pycon/sponsorship/forms.py new file mode 100644 index 00000000..70866ccf --- /dev/null +++ b/pycon/sponsorship/forms.py @@ -0,0 +1,72 @@ +from django import forms +from django.forms.models import inlineformset_factory, BaseInlineFormSet + +from django.contrib.admin.widgets import AdminFileWidget + +from pycon.sponsorship.models import Sponsor, SponsorBenefit + + +class SponsorApplicationForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user") + kwargs.update({ + "initial": { + "contact_name": self.user.get_full_name, + "contact_email": self.user.email, + } + }) + super(SponsorApplicationForm, self).__init__(*args, **kwargs) + + class Meta: + model = Sponsor + fields = ["name", "contact_name", "contact_email", "level"] + + def save(self, commit=True): + obj = super(SponsorApplicationForm, self).save(commit=False) + obj.applicant = self.user + if commit: + obj.save() + return obj + + +class SponsorDetailsForm(forms.ModelForm): + class Meta: + model = Sponsor + fields = [ + "name", + "external_url", + "contact_name", + "contact_email" + ] + + +class SponsorBenefitsInlineFormSet(BaseInlineFormSet): + + def _construct_form(self, i, **kwargs): + form = super(SponsorBenefitsInlineFormSet, self)._construct_form(i, **kwargs) + + # only include the relevant data fields for this benefit type + fields = form.instance.data_fields() + form.fields = dict((k, v) for (k, v) in form.fields.items() if k in fields + ["id"]) + + for field in fields: + # don't need a label, the form template will label it with the benefit name + form.fields[field].label = "" + + # provide word limit as help_text + if form.instance.benefit.type == "text" and form.instance.max_words: + form.fields[field].help_text = u"maximum %s words" % form.instance.max_words + + # use admin file widget that shows currently uploaded file + if field == "upload": + form.fields[field].widget = AdminFileWidget() + + return form + + +SponsorBenefitsFormSet = inlineformset_factory( + Sponsor, SponsorBenefit, + formset=SponsorBenefitsInlineFormSet, + can_delete=False, extra=0, + fields=["text", "upload"] +) diff --git a/pycon/sponsorship/management/__init__.py b/pycon/sponsorship/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pycon/sponsorship/management/commands/__init__.py b/pycon/sponsorship/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pycon/sponsorship/management/commands/create_sponsors_groups.py b/pycon/sponsorship/management/commands/create_sponsors_groups.py new file mode 100644 index 00000000..2a389e6a --- /dev/null +++ b/pycon/sponsorship/management/commands/create_sponsors_groups.py @@ -0,0 +1,12 @@ +from django.core.management.base import BaseCommand + +from django.contrib.auth.models import Group + +from pycon.sponsorship import AUTH_GROUPS + + +class Command(BaseCommand): + + def handle(self, *args, **options): + for group in AUTH_GROUPS: + Group.objects.get_or_create(name=group) diff --git a/pycon/sponsorship/management/commands/export_sponsors_data.py b/pycon/sponsorship/management/commands/export_sponsors_data.py new file mode 100644 index 00000000..f7df698c --- /dev/null +++ b/pycon/sponsorship/management/commands/export_sponsors_data.py @@ -0,0 +1,80 @@ +import csv +import os +import shutil +import zipfile + +from contextlib import closing + +from django.core.management.base import BaseCommand, CommandError +from django.template.defaultfilters import slugify + +from pycon.sponsorship.models import Sponsor + + +def zipdir(basedir, archivename): + assert os.path.isdir(basedir) + with closing(zipfile.ZipFile(archivename, "w", zipfile.ZIP_DEFLATED)) as z: + for root, dirs, files in os.walk(basedir): + #NOTE: ignore empty directories + for fn in files: + absfn = os.path.join(root, fn) + zfn = absfn[len(basedir)+len(os.sep):] #XXX: relative path + z.write(absfn, zfn) + + +class Command(BaseCommand): + + def handle(self, *args, **options): + try: + os.makedirs(os.path.join(os.getcwd(), "build")) + except: + pass + + csv_file = csv.writer( + open(os.path.join(os.getcwd(), "build", "sponsors.csv"), "wb") + ) + csv_file.writerow(["Name", "URL", "Level", "Description"]) + + for sponsor in Sponsor.objects.all(): + path = os.path.join(os.getcwd(), "build", slugify(sponsor.name)) + try: + os.makedirs(path) + except: + pass + + data = { + "name": sponsor.name, + "url": sponsor.external_url, + "level": sponsor.level.name, + "description": "", + } + for sponsor_benefit in sponsor.sponsor_benefits.all(): + if sponsor_benefit.benefit_id == 2: + data["description"] = sponsor_benefit.text + if sponsor_benefit.benefit_id == 1: + if sponsor_benefit.upload: + data["ad"] = sponsor_benefit.upload.path + if sponsor_benefit.benefit_id == 7: + if sponsor_benefit.upload: + data["logo"] = sponsor_benefit.upload.path + + if "ad" in data: + ad_path = data.pop("ad") + shutil.copy(ad_path, path) + if "logo" in data: + logo_path = data.pop("logo") + shutil.copy(logo_path, path) + + csv_file.writerow([ + data["name"].encode("utf-8"), + data["url"].encode("utf-8"), + data["level"].encode("utf-8"), + data["description"].encode("utf-8") + ]) + + zipdir( + os.path.join( + os.getcwd(), "build"), + os.path.join(os.getcwd(), "sponsors.zip" + ) + ) diff --git a/pycon/sponsorship/management/commands/reset_sponsor_benefits.py b/pycon/sponsorship/management/commands/reset_sponsor_benefits.py new file mode 100644 index 00000000..cb2e833f --- /dev/null +++ b/pycon/sponsorship/management/commands/reset_sponsor_benefits.py @@ -0,0 +1,38 @@ +from django.core.management.base import BaseCommand + +from django.contrib.auth.models import Group + +from pycon.sponsorship.models import Sponsor, SponsorBenefit + + +class Command(BaseCommand): + + def handle(self, *args, **options): + for sponsor in Sponsor.objects.all(): + level = None + try: + level = sponsor.level + except SponsorLevel.DoesNotExist: + pass + if level: + for benefit_level in level.benefit_levels.all(): + # Create all needed benefits if they don't exist already + sponsor_benefit, created = SponsorBenefit.objects.get_or_create( + sponsor=sponsor, benefit=benefit_level.benefit) + + if created: + print "created", sponsor_benefit, "for", sponsor + + # and set to default limits for this level. + sponsor_benefit.max_words = benefit_level.max_words + sponsor_benefit.other_limits = benefit_level.other_limits + + # and set to active + sponsor_benefit.active = True + + # @@@ We don't call sponsor_benefit.clean here. This means + # that if the sponsorship level for a sponsor is adjusted + # downwards, an existing too-long text entry can remain, + # and won't raise a validation error until it's next + # edited. + sponsor_benefit.save() diff --git a/pycon/sponsorship/managers.py b/pycon/sponsorship/managers.py new file mode 100644 index 00000000..637c311b --- /dev/null +++ b/pycon/sponsorship/managers.py @@ -0,0 +1,38 @@ +from django.db import models + + +class SponsorManager(models.Manager): + + def active(self): + return self.get_query_set().filter(active=True).order_by("level") + + def with_weblogo(self): + queryset = self.raw(""" + SELECT DISTINCT + "sponsorship_sponsor"."id", + "sponsorship_sponsor"."applicant_id", + "sponsorship_sponsor"."name", + "sponsorship_sponsor"."external_url", + "sponsorship_sponsor"."annotation", + "sponsorship_sponsor"."contact_name", + "sponsorship_sponsor"."contact_email", + "sponsorship_sponsor"."level_id", + "sponsorship_sponsor"."added", + "sponsorship_sponsor"."active", + "sponsorship_sponsorlevel"."order" + FROM + "sponsorship_sponsor" + INNER JOIN + "sponsorship_sponsorbenefit" ON ("sponsorship_sponsor"."id" = "sponsorship_sponsorbenefit"."sponsor_id") + INNER JOIN + "sponsorship_benefit" ON ("sponsorship_sponsorbenefit"."benefit_id" = "sponsorship_benefit"."id") + LEFT OUTER JOIN + "sponsorship_sponsorlevel" ON ("sponsorship_sponsor"."level_id" = "sponsorship_sponsorlevel"."id") + WHERE ( + "sponsorship_sponsor"."active" = 't' AND + "sponsorship_benefit"."type" = 'weblogo' AND + "sponsorship_sponsorbenefit"."upload" != '' + ) + ORDER BY "sponsorship_sponsorlevel"."order" ASC, "sponsorship_sponsor"."added" ASC + """) + return queryset diff --git a/pycon/sponsorship/models.py b/pycon/sponsorship/models.py new file mode 100644 index 00000000..8320b049 --- /dev/null +++ b/pycon/sponsorship/models.py @@ -0,0 +1,266 @@ +import datetime + +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse +from django.db import models +from django.db.models.signals import post_init, post_save +from django.utils.translation import ugettext_lazy as _ + +from django.contrib.auth.models import User + +from symposion.conference.models import Conference + +from pycon.sponsorship import SPONSOR_COORDINATORS +from pycon.sponsorship.managers import SponsorManager +# from symposion.utils.mail import send_email + + +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 u"%s %s" % (self.conference, 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) + + name = models.CharField(_("Sponsor Name"), max_length=100) + external_url = models.URLField(_("external URL")) + annotation = models.TextField(_("annotation"), blank=True) + contact_name = models.CharField(_("Contact Name"), max_length=100) + contact_email = models.EmailField(_(u"Contact Email")) + level = models.ForeignKey(SponsorLevel, verbose_name=_("level")) + added = models.DateTimeField(_("added"), default=datetime.datetime.now) + active = models.BooleanField(_("active"), default=False) + + # Denormalization (this assumes only one logo) + sponsor_logo = models.ForeignKey("SponsorBenefit", related_name="+", null=True, blank=True, editable=False) + + 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_url(self): + if not hasattr(self, "_website_logo_url"): + self._website_logo_url = None + benefits = self.sponsor_benefits.filter(benefit__type="weblogo", upload__isnull=False) + if benefits.exists(): + # @@@ smarter handling of multiple weblogo benefits? + # shouldn't happen + if benefits[0].upload: + self._website_logo_url = benefits[0].upload.url + return self._website_logo_url + + @property + def listing_text(self): + if not hasattr(self, "_listing_text"): + self._listing_text = None + benefits = self.sponsor_benefits.filter(benefit__id=7) + if benefits.count(): + self._listing_text = benefits[0].text + return self._listing_text + + @property + def joblisting_text(self): + if not hasattr(self, "_joblisting_text"): + self._joblisting_text = None + benefits = self.sponsor_benefits.filter(benefit__id=21) + if benefits.count(): + self._joblisting_text = benefits[0].text + return self._joblisting_text + + @property + def website_logo(self): + if self.sponsor_logo is None: + 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 + + 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="") + + # @@@ should this just be done centrally? + def send_coordinator_emails(self): + for user in User.objects.filter(groups__name=SPONSOR_COORDINATORS): + send_email( + [user.email], "sponsor_signup", + context = {"sponsor": self} + ) + + +def _store_initial_level(sender, instance, **kwargs): + if instance: + instance._initial_level_id = instance.level_id +post_init.connect(_store_initial_level, sender=Sponsor) + + +def _check_level_change(sender, instance, created, **kwargs): + if instance and (created or instance.level_id != instance._initial_level_id): + instance.reset_benefits() +post_save.connect(_check_level_change, sender=Sponsor) + + +def _send_sponsor_notification_emails(sender, instance, created, **kwargs): + if instance and created: + instance.send_coordinator_emails() +post_save.connect(_send_sponsor_notification_emails, sender=Sponsor) + + +class Benefit(models.Model): + + name = models.CharField(_("name"), max_length=100) + description = models.TextField(_("description"), blank=True) + type = models.CharField( + _("type"), + choices=[ + ("text", "Text"), + ("file", "File"), + ("weblogo", "Web Logo"), + ("simple", "Simple") + ], + 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") + ) + 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): + if self.max_words and len(self.text.split()) > self.max_words: + raise ValidationError("Sponsorship level only allows for %s words." % self.max_words) + + def data_fields(self): + """ + Return list of data field names which should be editable for + this ``SponsorBenefit``, depending on its ``Benefit`` type. + """ + if self.benefit.type == "file" or self.benefit.type == "weblogo": + return ["upload"] + elif self.benefit.type == "text": + return ["text"] + return [] + + +def _denorm_weblogo(sender, instance, created, **kwargs): + if instance: + if instance.benefit.type == "weblogo" and instance.upload: + sponsor = instance.sponsor + sponsor.sponsor_logo = instance + sponsor.save() +post_save.connect(_denorm_weblogo, sender=SponsorBenefit) \ No newline at end of file diff --git a/pycon/sponsorship/templatetags/__init__.py b/pycon/sponsorship/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pycon/sponsorship/templatetags/sponsorship_tags.py b/pycon/sponsorship/templatetags/sponsorship_tags.py new file mode 100644 index 00000000..9a4ba982 --- /dev/null +++ b/pycon/sponsorship/templatetags/sponsorship_tags.py @@ -0,0 +1,74 @@ +from django import template + +from symposion.conference.models import current_conference +from pycon.sponsorship.models import Sponsor, SponsorLevel + + +register = template.Library() + + +class SponsorsNode(template.Node): + + @classmethod + def handle_token(cls, parser, token): + bits = token.split_contents() + if len(bits) == 3 and bits[1] == "as": + return cls(bits[2]) + elif len(bits) == 4 and bits[2] == "as": + return cls(bits[3], bits[1]) + else: + raise template.TemplateSyntaxError("%r takes 'as var' or 'level as var'" % bits[0]) + + def __init__(self, context_var, level=None): + if level: + self.level = template.Variable(level) + else: + self.level = None + self.context_var = context_var + + def render(self, context): + conference = current_conference() + if self.level: + level = self.level.resolve(context) + queryset = Sponsor.objects.filter(level__conference = conference, level__name__iexact = level, active = True).order_by("added") + else: + queryset = Sponsor.objects.filter(level__conference = conference, active = True).order_by("level__order", "added") + context[self.context_var] = queryset + return u"" + + +class SponsorLevelNode(template.Node): + + @classmethod + def handle_token(cls, parser, token): + bits = token.split_contents() + if len(bits) == 3 and bits[1] == "as": + return cls(bits[2]) + else: + raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0]) + + def __init__(self, context_var): + self.context_var = context_var + + def render(self, context): + conference = current_conference() + context[self.context_var] = SponsorLevel.objects.filter(conference=conference) + return u"" + + +@register.tag +def sponsors(parser, token): + """ + {% sponsors as all_sponsors %} + or + {% sponsors "gold" as gold_sponsors %} + """ + return SponsorsNode.handle_token(parser, token) + + +@register.tag +def sponsor_levels(parser, token): + """ + {% sponsor_levels as levels %} + """ + return SponsorLevelNode.handle_token(parser, token) diff --git a/pycon/sponsorship/urls.py b/pycon/sponsorship/urls.py new file mode 100644 index 00000000..7776d200 --- /dev/null +++ b/pycon/sponsorship/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls.defaults import patterns, url +from django.views.generic.simple import direct_to_template + + +urlpatterns = patterns("pycon.sponsorship.views", + url(r"^$", direct_to_template, {"template": "sponsorship/list.html"}, name="sponsor_list"), + # url(r"^jobs/$", direct_to_template, {"template": "sponsors/jobs.html"}, name="sponsor_jobs"), + url(r"^apply/$", "sponsor_apply", name="sponsor_apply"), + url(r"^(?P\d+)/$", "sponsor_detail", name="sponsor_detail"), +) diff --git a/pycon/sponsorship/views.py b/pycon/sponsorship/views.py new file mode 100644 index 00000000..12896b22 --- /dev/null +++ b/pycon/sponsorship/views.py @@ -0,0 +1,115 @@ +import itertools + +from functools import wraps + +from django.http import HttpResponse +from django.shortcuts import render_to_response, redirect, get_object_or_404 +from django.template import RequestContext + +from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required +from django.contrib.auth.decorators import login_required + +from pycon.sponsorship.forms import SponsorApplicationForm, SponsorDetailsForm, SponsorBenefitsFormSet +from pycon.sponsorship.models import Sponsor, SponsorBenefit + + +@login_required +def sponsor_apply(request): + if request.method == "POST": + form = SponsorApplicationForm(request.POST, user=request.user) + if form.is_valid(): + form.save() + return redirect("dashboard") + else: + form = SponsorApplicationForm(user=request.user) + + return render_to_response("sponsorship/apply.html", { + "form": form, + }, context_instance=RequestContext(request)) + + +@login_required +def sponsor_detail(request, pk): + sponsor = get_object_or_404(Sponsor, pk=pk) + + if not sponsor.active or 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, "Your sponsorship application has been submitted!") + + return redirect(request.path) + else: + form = SponsorDetailsForm(instance=sponsor) + formset = SponsorBenefitsFormSet(**formset_kwargs) + + return render_to_response("sponsorship/detail.html", { + "sponsor": sponsor, + "form": form, + "formset": formset, + }, context_instance=RequestContext(request)) + + +@staff_member_required +def sponsor_export_data(request): + sponsors = [] + data = "" + + for sponsor in Sponsor.objects.order_by("added"): + d = { + "name": sponsor.name, + "url": sponsor.external_url, + "level": (sponsor.level.order, sponsor.level.name), + "description": "", + } + for sponsor_benefit in sponsor.sponsor_benefits.all(): + if sponsor_benefit.benefit_id == 2: + d["description"] = sponsor_benefit.text + sponsors.append(d) + + def izip_longest(*args): + fv = None + def sentinel(counter=([fv]*(len(args)-1)).pop): + yield counter() + iters = [itertools.chain(it, sentinel(), itertools.repeat(fv)) for it in args] + try: + for tup in itertools.izip(*iters): + yield tup + except IndexError: + pass + def pairwise(iterable): + a, b = itertools.tee(iterable) + b.next() + return izip_longest(a, b) + + def level_key(s): + return s["level"] + + for level, level_sponsors in itertools.groupby(sorted(sponsors, key=level_key), level_key): + data += "%s\n" % ("-" * (len(level[1])+4)) + data += "| %s |\n" % level[1] + data += "%s\n\n" % ("-" * (len(level[1])+4)) + for sponsor, next in pairwise(level_sponsors): + description = sponsor["description"].strip() + description = description if description else "-- NO DESCRIPTION FOR THIS SPONSOR --" + data += "%s\n\n%s" % (sponsor["name"], description) + if next is not None: + data += "\n\n%s\n\n" % ("-"*80) + else: + data += "\n\n" + + return HttpResponse(data, content_type="text/plain;charset=utf-8")