From da82edf26d1bc4db8f3020853a725f4aaafa2f48 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sun, 21 Jun 2015 11:11:04 +0900 Subject: [PATCH 1/3] Enhanced sponsorship features MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It is a part of feedback from PyConJP development. Here picks a sponsorship benefit management enhancement. * sponsor zip download * export sponsor data as csv ``` commit eb3261c12c910ec562e016f10431cc48747baef8 Author: Dan Poirier Date: Wed Aug 21 11:51:20 2013 -0400 Enhanced sponsor admin page For #67: * admin list sorted by name * not limited to 100 per page * a sortable visual indicator for each sponsorship benefit (completed, missing, not applicable) * also the sponsorship level, admin contact name, and an active or not indication * the ones we have today: print logo, web logo, print description, web description and the ad. * an action pick list like “email” and check mark the sponsors * I want to email based on the assets that are missing. * in subject and body, replace %%NAME%% by sponsor name ``` Signed-off-by: Hiroshi Miura --- symposion/sponsorship/urls.py | 1 + symposion/sponsorship/views.py | 126 ++++++++++++++++++++++++++++++--- 2 files changed, 118 insertions(+), 9 deletions(-) diff --git a/symposion/sponsorship/urls.py b/symposion/sponsorship/urls.py index 17db5124..d7027fea 100644 --- a/symposion/sponsorship/urls.py +++ b/symposion/sponsorship/urls.py @@ -7,5 +7,6 @@ urlpatterns = patterns( url(r"^$", TemplateView.as_view(template_name="sponsorship/list.html"), name="sponsor_list"), url(r"^apply/$", "sponsor_apply", name="sponsor_apply"), url(r"^add/$", "sponsor_add", name="sponsor_add"), + url(r"^ziplogos/$", "sponsor_zip_logo_files", name="sponsor_zip_logos"), url(r"^(?P\d+)/$", "sponsor_detail", name="sponsor_detail"), ) diff --git a/symposion/sponsorship/views.py b/symposion/sponsorship/views.py index e3b22697..bf74ec3a 100644 --- a/symposion/sponsorship/views.py +++ b/symposion/sponsorship/views.py @@ -1,13 +1,28 @@ -from django.http import Http404 +from cStringIO import StringIO +import itertools +import logging +import os +import time +from zipfile import ZipFile, ZipInfo + +from django.conf import settings + +from django.http import Http404, HttpResponse from django.shortcuts import render_to_response, redirect, get_object_or_404 from django.template import RequestContext +from django.utils.translation import ugettext_lazy as _ from django.contrib import messages +from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth.decorators import login_required -from symposion.sponsorship.forms import SponsorApplicationForm, SponsorDetailsForm, \ - SponsorBenefitsFormSet -from symposion.sponsorship.models import Sponsor, SponsorBenefit +from symposion.sponsorship.forms import SponsorApplicationForm, \ + SponsorDetailsForm, SponsorBenefitsFormSet +from symposion.sponsorship.models import Benefit, Sponsor, SponsorBenefit, \ + SponsorLevel + + +log = logging.getLogger(__name__) @login_required @@ -18,13 +33,13 @@ def sponsor_apply(request): sponsor = form.save() if sponsor.sponsor_benefits.all(): # Redirect user to sponsor_detail to give extra information. - messages.success(request, "Thank you for your sponsorship " - "application. Please update your " - "benefit details below.") + messages.success(request, _("Thank you for your sponsorship " + "application. Please update your " + "benefit details below.")) return redirect("sponsor_detail", pk=sponsor.pk) else: - messages.success(request, "Thank you for your sponsorship " - "application.") + messages.success(request, _("Thank you for your sponsorship " + "application.")) return redirect("dashboard") else: form = SponsorApplicationForm(user=request.user) @@ -87,3 +102,96 @@ def sponsor_detail(request, pk): "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") + + +@staff_member_required +def sponsor_zip_logo_files(request): + """Return a zip file of sponsor web and print logos""" + + zip_stringio = StringIO() + zipfile = ZipFile(zip_stringio, "w") + try: + benefits = Benefit.objects.all() + for benefit in benefits: + dir_name = benefit.name.lower().replace(" ", "_").replace('/', '_') + for level in SponsorLevel.objects.all(): + level_name = level.name.lower().replace(" ", "_").replace('/', '_') + for sponsor in Sponsor.objects.filter(level=level, active=True): + sponsor_name = sponsor.name.lower().replace(" ", "_").replace('/', '_') + full_dir = "/".join([dir_name, level_name, sponsor_name]) + for sponsor_benefit in SponsorBenefit.objects.filter( + benefit=benefit, + sponsor=sponsor, + active=True, + ).exclude(upload=''): + if os.path.exists(sponsor_benefit.upload.path): + modtime = time.gmtime(os.stat(sponsor_benefit.upload.path).st_mtime) + with open(sponsor_benefit.upload.path, "rb") as f: + fname = os.path.split(sponsor_benefit.upload.name)[-1] + zipinfo = ZipInfo(filename=full_dir + "/" + fname, + date_time=modtime) + zipfile.writestr(zipinfo, f.read()) + else: + log.debug("No such sponsor file: %s" % sponsor_benefit.upload.path) + finally: + zipfile.close() + + response = HttpResponse(zip_stringio.getvalue(), + content_type="application/zip") + prefix = settings.CONFERENCE_URL_PREFIXES[settings.CONFERENCE_ID] + response['Content-Disposition'] = \ + 'attachment; filename="%s_sponsorlogos.zip"' % prefix + return response From fbf356b335dea6673dfca2ca61d0e2bbd6aed173 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sun, 21 Jun 2015 13:40:49 +0900 Subject: [PATCH 2/3] add export sponsor data zip command Signed-off-by: Hiroshi Miura --- .../commands/export_sponsors_data.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 symposion/sponsorship/management/commands/export_sponsors_data.py diff --git a/symposion/sponsorship/management/commands/export_sponsors_data.py b/symposion/sponsorship/management/commands/export_sponsors_data.py new file mode 100644 index 00000000..61172d44 --- /dev/null +++ b/symposion/sponsorship/management/commands/export_sponsors_data.py @@ -0,0 +1,77 @@ +import csv +import os +import shutil +import zipfile + +from contextlib import closing + +from django.core.management.base import BaseCommand +from django.template.defaultfilters import slugify + +from sotmjp.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")) From 999c458c1b237bb9b2442cefeef825a7a4c82a71 Mon Sep 17 00:00:00 2001 From: Hiroshi Miura Date: Sun, 21 Jun 2015 13:57:05 +0900 Subject: [PATCH 3/3] add test Signed-off-by: Hiroshi Miura --- symposion/sponsorship/tests.py | 307 +++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 symposion/sponsorship/tests.py diff --git a/symposion/sponsorship/tests.py b/symposion/sponsorship/tests.py new file mode 100644 index 00000000..a6eae66b --- /dev/null +++ b/symposion/sponsorship/tests.py @@ -0,0 +1,307 @@ +from cStringIO import StringIO +import os +import shutil +import tempfile +from zipfile import ZipFile + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.core.urlresolvers import reverse +from django.test import TestCase +from django.test.utils import override_settings + +from pycon.sponsorship.models import Benefit, Sponsor, SponsorBenefit,\ + SponsorLevel +from symposion.conference.models import current_conference + + +class TestSponsorZipDownload(TestCase): + def setUp(self): + self.user = User.objects.create_user(username='joe', + email='joe@example.com', + password='joe') + self.user.is_staff = True + self.user.save() + self.url = reverse("sponsor_zip_logos") + self.assertTrue(self.client.login(username='joe@example.com', + password='joe')) + + # we need a sponsor + conference = current_conference() + self.sponsor_level = SponsorLevel.objects.create( + conference=conference, name="Lead", cost=1) + self.sponsor = Sponsor.objects.create( + name="Big Daddy", + level=self.sponsor_level, + active=True, + ) + + # Create our benefits, of various types + self.text_benefit = Benefit.objects.create(name="text", type="text") + self.file_benefit = Benefit.objects.create(name="file", type="file") + # These names must be spelled exactly this way: + self.weblogo_benefit = Benefit.objects.create(name="Web logo", type="weblogo") + self.printlogo_benefit = Benefit.objects.create(name="Print logo", type="file") + self.advertisement_benefit = Benefit.objects.create(name="Advertisement", type="file") + + def validate_response(self, rsp, names_and_sizes): + # Ensure a response from the view looks right, contains a valid + # zip archive, has files with the right names and sizes. + self.assertEqual("application/zip", rsp['Content-type']) + prefix = settings.CONFERENCE_URL_PREFIXES[settings.CONFERENCE_ID] + + self.assertEqual( + 'attachment; filename="pycon_%s_sponsorlogos.zip"' % prefix, + rsp['Content-Disposition']) + zipfile = ZipFile(StringIO(rsp.content), "r") + # Check out the zip - testzip() returns None if no errors found + self.assertIsNone(zipfile.testzip()) + # Compare contents to what is expected + infolist = zipfile.infolist() + self.assertEqual(len(names_and_sizes), len(infolist)) + for info, name_and_size in zip(infolist, names_and_sizes): + name, size = name_and_size + self.assertEqual(name, info.filename) + self.assertEqual(size, info.file_size) + + def make_temp_file(self, name, size=0): + # Create a temp file with the given name and size under self.temp_dir + path = os.path.join(self.temp_dir, name) + with open(path, "wb") as f: + f.write(size * "x") + + def test_must_be_logged_in(self): + # Must be logged in to use the view + # If not logged in, doesn't redirect, just serves up a login view + self.client.logout() + rsp = self.client.get(self.url) + self.assertEqual(200, rsp.status_code) + self.assertIn("""""", rsp.content) + + def test_must_be_staff(self): + # Only staff can use the view + # If not staff, doesn't show error, just serves up a login view + # Also, the dashboard doesn't show the download button + self.user.is_staff = False + self.user.save() + rsp = self.client.get(self.url) + self.assertEqual(200, rsp.status_code) + self.assertIn("""""", rsp.content) + rsp = self.client.get(reverse('dashboard')) + self.assertNotIn(self.url, rsp.content) + + def test_no_files(self): + # If there are no sponsor files, we still work + # And the dashboard shows our download button + rsp = self.client.get(self.url) + self.validate_response(rsp, []) + rsp = self.client.get(reverse('dashboard')) + self.assertIn(self.url, rsp.content) + + def test_different_benefit_types(self): + # We only get files from the benefits named "Print logo" and "Web logo" + # And we ignore any non-existent files + try: + # Create a temp dir for media files + self.temp_dir = tempfile.mkdtemp() + with override_settings(MEDIA_ROOT=self.temp_dir): + + # Give our sponsor some benefits + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.text_benefit, + text="Foo!" + ) + + self.make_temp_file("file1", 10) + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.file_benefit, + upload="file1" + ) + + self.make_temp_file("file2", 20) + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.weblogo_benefit, + upload="file2" + ) + + # Benefit whose file is missing from the disk + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.weblogo_benefit, + upload="file3" + ) + + # print logo benefit + self.make_temp_file("file4", 40) + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.printlogo_benefit, + upload="file4" + ) + + self.make_temp_file("file5", 50) + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.advertisement_benefit, + upload="file5" + ) + + rsp = self.client.get(self.url) + expected = [ + ('web_logos/lead/big_daddy/file2', 20), + ('print_logos/lead/big_daddy/file4', 40), + ('advertisement/lead/big_daddy/file5', 50) + ] + self.validate_response(rsp, expected) + finally: + if hasattr(self, 'temp_dir'): + # Clean up any temp media files + shutil.rmtree(self.temp_dir) + + def test_file_org(self): + # The zip file is organized into directories: + # {print_logos,web_logos,advertisement}/// + + # Add another sponsor at a different sponsor level + conference = current_conference() + self.sponsor_level2 = SponsorLevel.objects.create( + conference=conference, name="Silly putty", cost=1) + self.sponsor2 = Sponsor.objects.create( + name="Big Mama", + level=self.sponsor_level2, + active=True, + ) + # + try: + # Create a temp dir for media files + self.temp_dir = tempfile.mkdtemp() + with override_settings(MEDIA_ROOT=self.temp_dir): + + # Give our sponsors some benefits + self.make_temp_file("file1", 10) + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.weblogo_benefit, + upload="file1" + ) + # print logo benefit + self.make_temp_file("file2", 20) + SponsorBenefit.objects.create( + sponsor=self.sponsor, + benefit=self.printlogo_benefit, + upload="file2" + ) + # Sponsor 2 + self.make_temp_file("file3", 30) + SponsorBenefit.objects.create( + sponsor=self.sponsor2, + benefit=self.weblogo_benefit, + upload="file3" + ) + # print logo benefit + self.make_temp_file("file4", 42) + SponsorBenefit.objects.create( + sponsor=self.sponsor2, + benefit=self.printlogo_benefit, + upload="file4" + ) + # ad benefit + self.make_temp_file("file5", 55) + SponsorBenefit.objects.create( + sponsor=self.sponsor2, + benefit=self.advertisement_benefit, + upload="file5" + ) + + rsp = self.client.get(self.url) + expected = [ + ('web_logos/lead/big_daddy/file1', 10), + ('web_logos/silly_putty/big_mama/file3', 30), + ('print_logos/lead/big_daddy/file2', 20), + ('print_logos/silly_putty/big_mama/file4', 42), + ('advertisement/silly_putty/big_mama/file5', 55), + ] + self.validate_response(rsp, expected) + finally: + if hasattr(self, 'temp_dir'): + # Clean up any temp media files + shutil.rmtree(self.temp_dir) + + +class TestBenefitValidation(TestCase): + """ + It should not be possible to save a SponsorBenefit if it has the + wrong kind of data in it - e.g. a text-type benefit cannot have + an uploaded file, and vice-versa. + """ + def setUp(self): + # we need a sponsor + conference = current_conference() + self.sponsor_level = SponsorLevel.objects.create( + conference=conference, name="Lead", cost=1) + self.sponsor = Sponsor.objects.create( + name="Big Daddy", + level=self.sponsor_level, + ) + + # Create our benefit types + self.text_type = Benefit.objects.create(name="text", type="text") + self.file_type = Benefit.objects.create(name="file", type="file") + self.weblogo_type = Benefit.objects.create(name="log", type="weblogo") + self.simple_type = Benefit.objects.create(name="simple", type="simple") + + def validate(self, should_work, benefit_type, upload, text): + obj = SponsorBenefit( + benefit=benefit_type, + sponsor=self.sponsor, + upload=upload, + text=text + ) + if should_work: + obj.save() + else: + with self.assertRaises(ValidationError): + obj.save() + + def test_text_has_text(self): + self.validate(True, self.text_type, upload=None, text="Some text") + + def test_text_has_upload(self): + self.validate(False, self.text_type, upload="filename", text='') + + def test_text_has_both(self): + self.validate(False, self.text_type, upload="filename", text="Text") + + def test_file_has_text(self): + self.validate(False, self.file_type, upload=None, text="Some text") + + def test_file_has_upload(self): + self.validate(True, self.file_type, upload="filename", text='') + + def test_file_has_both(self): + self.validate(False, self.file_type, upload="filename", text="Text") + + def test_weblogo_has_text(self): + self.validate(False, self.weblogo_type, upload=None, text="Some text") + + def test_weblogo_has_upload(self): + self.validate(True, self.weblogo_type, upload="filename", text='') + + def test_weblogo_has_both(self): + self.validate(False, self.weblogo_type, upload="filename", text="Text") + + def test_simple_has_neither(self): + self.validate(True, self.simple_type, upload=None, text='') + + def test_simple_has_text(self): + self.validate(True, self.simple_type, upload=None, text="Some text") + + def test_simple_has_upload(self): + self.validate(False, self.simple_type, upload="filename", text='') + + def test_simple_has_both(self): + self.validate(False, self.simple_type, upload="filename", text="Text")