Merge branch 'master' into schedule-admin

Conflicts:
	symposion/schedule/admin.py
This commit is contained in:
Patrick Altman 2015-09-08 14:51:49 -05:00
commit 8a95b0861c
18 changed files with 1285 additions and 432 deletions

View file

@ -1,3 +1,4 @@
include README LICENSE include README LICENSE
recursive-include symposion/templates *.html *.txt recursive-include symposion/templates *.html *.txt
recursive-include symposion/static * recursive-include symposion/static *
recursive-include symposion/locale *

View file

@ -1,6 +1,9 @@
Symposion Symposion
--------- ---------
.. image:: http://slack.pinaxproject.com/badge.svg
:target: http://slack.pinaxproject.com/
.. image:: https://img.shields.io/travis/pinax/symposion.svg .. image:: https://img.shields.io/travis/pinax/symposion.svg
:target: https://travis-ci.org/pinax/symposion :target: https://travis-ci.org/pinax/symposion
@ -17,12 +20,18 @@ Symposion
:target: https://pypi.python.org/pypi/symposion/ :target: https://pypi.python.org/pypi/symposion/
Pinax
------
A conference management solution from Eldarion. Pinax is an open-source platform built on the Django Web Framework. It is an ecosystem of reusable Django apps, themes, and starter project templates.
This collection can be found at http://pinaxproject.com.
Built with the generous support of the Python Software Foundation.
See http://eldarion.com/symposion/ for commercial support, customization and hosting symposion
----------
symposion is a conference management solution from Eldarion. It was built with the generous support of the Python Software Foundation. See http://eldarion.com/symposion/ for commercial support, customization and hosting.
Quickstart Quickstart
========== ==========
@ -36,3 +45,23 @@ customize and manage your Symposion installation. We have built a [basic
Django startproject template that includes Symposion][1]. Django startproject template that includes Symposion][1].
[1]: https://github.com/pinax/pinax-project-symposion [1]: https://github.com/pinax/pinax-project-symposion
Documentation
---------------
The Pinax documentation is available at http://pinaxproject.com/pinax/.
Code of Conduct
----------------
In order to foster a kind, inclusive, and harassment-free community, the Pinax Project has a code of conduct, which can be found here http://pinaxproject.com/pinax/code_of_conduct/.
Pinax Project Blog and Twitter
-------------------------------
For updates and news regarding the Pinax Project, please follow us on Twitter at @pinaxproject and check out our blog http://blog.pinaxproject.com.

View file

@ -3,7 +3,18 @@ from django.contrib import admin
from symposion.conference.models import Conference, Section from symposion.conference.models import Conference, Section
admin.site.register(Conference, list_display=("title", "start_date", "end_date")) class SectionInline(admin.TabularInline):
model = Section
prepopulated_fields = {"slug": ("name",)}
extra = 1
class ConferenceAdmin(admin.ModelAdmin):
list_display = ("title", "start_date", "end_date")
inlines = [SectionInline, ]
admin.site.register(Conference, ConferenceAdmin)
admin.site.register( admin.site.register(
Section, Section,
prepopulated_fields={"slug": ("name",)}, prepopulated_fields={"slug": ("name",)},

View file

@ -1,3 +1,8 @@
try:
from collections import OrderedDict
except ImportError:
OrderedDict = None
from django import forms from django import forms
import account.forms import account.forms
@ -11,8 +16,7 @@ class SignupForm(account.forms.SignupForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(SignupForm, self).__init__(*args, **kwargs) super(SignupForm, self).__init__(*args, **kwargs)
del self.fields["username"] key_order = [
self.fields.keyOrder = [
"email", "email",
"email_confirm", "email_confirm",
"first_name", "first_name",
@ -20,6 +24,7 @@ class SignupForm(account.forms.SignupForm):
"password", "password",
"password_confirm" "password_confirm"
] ]
self.fields = reorder_fields(self.fields, key_order)
def clean_email_confirm(self): def clean_email_confirm(self):
email = self.cleaned_data.get("email") email = self.cleaned_data.get("email")
@ -29,3 +34,24 @@ class SignupForm(account.forms.SignupForm):
raise forms.ValidationError( raise forms.ValidationError(
"Email address must match previously typed email address") "Email address must match previously typed email address")
return email_confirm return email_confirm
def reorder_fields(fields, order):
"""Reorder form fields by order, removing items not in order.
>>> reorder_fields(
... OrderedDict([('a', 1), ('b', 2), ('c', 3)]),
... ['b', 'c', 'a'])
OrderedDict([('b', 2), ('c', 3), ('a', 1)])
"""
for key, v in fields.items():
if key not in order:
del fields[key]
if not OrderedDict or hasattr(fields, "keyOrder"):
# fields is SortedDict
fields.keyOrder.sort(key=lambda k: order.index(k[0]))
return fields
else:
# fields is OrderedDict
return OrderedDict(sorted(fields.items(), key=lambda k: order.index(k[0])))

Binary file not shown.

View file

@ -0,0 +1,573 @@
# Symposion japanese translation.
# Copyright (C) 2014-2015, pinax team
# This file is distributed under the same license as the symposion package.
# Hiroshi Miura <miurahr@linux.com>, 2015.
#
msgid ""
msgstr ""
"Project-Id-Version: symposion\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-31 14:47-0600\n"
"PO-Revision-Date: 2015-06-18 10:04+0900\n"
"Last-Translator: Hiroshi Miura <miurahr@linux.com>\n"
"Language-Team: Japanese translation team <https://www.transifex.com/projects/p/symposion/language/ja/>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.5.4\n"
"Language: ja\n"
#: cms/models.py:23
msgid "Draft"
msgstr "草稿"
#: cms/models.py:24
msgid "Public"
msgstr "公開"
#: cms/models.py:57
msgid "Path can only contain letters, numbers and hyphens and end with /"
msgstr ""
"パス名には、英数字、ハイフン(-)のみが使えます。また末尾は'/'の必要がありま"
"す。"
#: conference/models.py:15
msgid "title"
msgstr "タイトル"
#: conference/models.py:18 conference/models.py:58
msgid "start date"
msgstr "開始日"
#: conference/models.py:19 conference/models.py:59
msgid "end date"
msgstr "終了日"
#: conference/models.py:22
msgid "timezone"
msgstr "タイムゾーン"
#: conference/models.py:41 conference/models.py:52 sponsorship/models.py:18
msgid "conference"
msgstr "カンファレンス"
#: conference/models.py:42
msgid "conferences"
msgstr "カンファレンス"
#: conference/models.py:54 sponsorship/models.py:19 sponsorship/models.py:155
msgid "name"
msgstr "名前"
#: conference/models.py:65
msgid "section"
msgstr "セクション"
#: conference/models.py:66
msgid "sections"
msgstr "セクション"
#: proposals/models.py:71 templates/conference/user_list.html:60
msgid "Name"
msgstr "名前"
#: proposals/models.py:86
msgid "Brief Description"
msgstr "概要"
#: proposals/models.py:88
msgid ""
"If your proposal is accepted this will be made public and printed in the "
"program. Should be one paragraph, maximum 400 characters."
msgstr ""
"もし提案が受諾されたら、これは公開され、配布されるプログラムに印刷されます。"
"段落は一つのみで、400文字以内で記載してください。"
#: proposals/models.py:92
msgid "Detailed Abstract"
msgstr "提案の詳細"
#: proposals/models.py:93
msgid ""
"Detailed outline. Will be made public if your proposal is accepted. Edit "
"using <a href='http://daringfireball.net/projects/markdown/basics' "
"target='_blank'>Markdown</a>."
msgstr ""
"講演詳細:提案が受諾された場合に公開されます。<a href=\"http://www.markdown."
"jp/what-is-markdown/\" target='_blank'>Markdown</a>を用いて記述してください。"
#: proposals/models.py:99
msgid ""
"Anything else you'd like the program committee to know when making their "
"selection: your past experience, etc. This is not made public. Edit using <a "
"href='http://daringfireball.net/projects/markdown/basics' "
"target='_blank'>Markdown</a>."
msgstr ""
"プログラム委員に、講演者の過去の経験など、とくに伝えたいことがあれば、記述し"
"てください。これは公開されることはありません。<a href=\"http://www.markdown."
"jp/what-is-markdown/\" target='_blank'>Markdown</a>を用いて記述してください。"
#: proposals/models.py:153
msgid "Pending"
msgstr "ペンディング"
#: proposals/models.py:154 templates/proposals/_pending_proposal_row.html:16
msgid "Accepted"
msgstr "アクセプト"
#: proposals/models.py:155
msgid "Declined"
msgstr "リジェクト"
#: sponsorship/models.py:20
msgid "order"
msgstr "順序"
#: sponsorship/models.py:21
msgid "cost"
msgstr "経費"
#: sponsorship/models.py:22 sponsorship/models.py:156
msgid "description"
msgstr "記述"
#: sponsorship/models.py:22
msgid "This is private."
msgstr "これは非公開です。"
#: sponsorship/models.py:26
msgid "sponsor level"
msgstr "スポンサーのグレード"
#: sponsorship/models.py:27
msgid "sponsor levels"
msgstr "スポンサーのグレード"
#: sponsorship/models.py:38
msgid "applicant"
msgstr "応募者"
#: sponsorship/models.py:41
msgid "Sponsor Name"
msgstr "スポンサー名"
#: sponsorship/models.py:42
msgid "external URL"
msgstr "外部URL"
#: sponsorship/models.py:43
msgid "annotation"
msgstr ""
#: sponsorship/models.py:44
msgid "Contact Name"
msgstr "連絡先の名前"
#: sponsorship/models.py:45
msgid "Contact Email"
msgstr "連絡先のEmail"
#: sponsorship/models.py:46 sponsorship/models.py:167
msgid "level"
msgstr "グレード"
#: sponsorship/models.py:47
msgid "added"
msgstr ""
#: sponsorship/models.py:48
msgid "active"
msgstr "有効"
#: sponsorship/models.py:60 sponsorship/models.py:182
msgid "sponsor"
msgstr "スポンサー"
#: sponsorship/models.py:61
msgid "sponsors"
msgstr "スポンサー"
#: sponsorship/models.py:157
msgid "type"
msgstr "タイプ"
#: sponsorship/models.py:166 sponsorship/models.py:183
msgid "benefit"
msgstr ""
#: sponsorship/models.py:170 sponsorship/models.py:187
msgid "max words"
msgstr ""
#: sponsorship/models.py:171 sponsorship/models.py:188
msgid "other limits"
msgstr "他の制限"
#: sponsorship/models.py:192
msgid "text"
msgstr "テキスト"
#: sponsorship/models.py:193
msgid "file"
msgstr "ファイル"
#: templates/dashboard.html:16
msgid "Speaking"
msgstr "講演"
#: templates/dashboard.html:92 templates/sponsorship/detail.html:8
msgid "Sponsorship"
msgstr "スポンサー"
#: templates/dashboard.html:132 templates/reviews/review_detail.html:75
msgid "Reviews"
msgstr "レビュー"
#: templates/dashboard.html:177
msgid "Teams"
msgstr "チーム"
#: templates/boxes/box.html:9
msgid "Editing content:"
msgstr "コンテンツ編集:"
#: templates/cms/page_edit.html:11
msgid "Edit page at:"
msgstr ""
#: templates/conference/user_list.html:59
msgid "Email"
msgstr "電子メール"
#: templates/conference/user_list.html:61
msgid "Speaker Profile?"
msgstr "講演者のプロフィール"
#: templates/emails/teams_user_applied/message.html:3
#, python-format
msgid ""
"\n"
" <p>\n"
" User \"%(username)s\" has applied to join <b>%(team_name)s</b> on "
"%(site_name)s.\n"
" </p>\n"
"\n"
" <p>\n"
" To accept this application and see any other pending applications, "
"visit the following url:\n"
" <a href=\"http://%(site_url)s%(team_url)s\">http://%(site_url)s"
"%(team_url)s</a>\n"
" </p>\n"
msgstr ""
"\n"
" <p>\n"
" ユーザ \"%(username)s\" は、%(site_name)s の<b>%(team_name)s</b> に"
"応募しました。 \n"
" </p>\n"
"\n"
" <p>\n"
" この応募を受諾したり、他の保留されている応募者を確認するには、つぎの"
"URLを参照してください:\n"
" <a href=\"http://%(site_url)s%(team_url)s\">http://%(site_url)s"
"%(team_url)s</a>\n"
" </p>\n"
#: templates/emails/teams_user_applied/subject.txt:1
#, python-format
msgid "%(username)s has applied to to join \"%(team)s\""
msgstr "%(username)s は、 \"%(team)s\"に応募しました。"
#: templates/emails/teams_user_invited/message.html:3
#, python-format
msgid ""
"\n"
" <p>\n"
" You have been invited to join <b>%(team_name)s</b> on "
"%(site_name)s.\n"
" </p>\n"
"\n"
" <p>\n"
" To accept this invitation, visit the following url:\n"
" <a href=\"http://%(site_url)s%(team_url)s\">http://%(site_url)s"
"%(team_url)s</a>\n"
" </p>\n"
msgstr ""
"\n"
" <p>\n"
" %(site_name)sの<b>%(team_name)s</b>に参加するよう招待されました。\n"
" </p>\n"
"\n"
" <p>\n"
" 招待を受諾するには、つぎのURLをクリックしてください:\n"
" <a href=\"http://%(site_url)s%(team_url)s\">http://%(site_url)s"
"%(team_url)s</a>\n"
" </p>\n"
#: templates/emails/teams_user_invited/subject.txt:1
#, python-format
msgid "You have been invited to join \"%(team)s\""
msgstr " \"%(team)s\"に参加するよう招待されました。"
#: templates/proposals/_pending_proposal_row.html:12
msgid "Cancelled"
msgstr "キャンセル済み"
#: templates/proposals/_pending_proposal_row.html:18
msgid "Submitted"
msgstr "投稿済み"
#: templates/proposals/_pending_proposal_row.html:21
msgid "Invited"
msgstr "招待された"
#: templates/proposals/_pending_proposal_row.html:30
msgid "Choose Response"
msgstr "応答の選択"
#: templates/proposals/_pending_proposal_row.html:35
msgid "Accept invitation"
msgstr "招待を受諾"
#: templates/proposals/_pending_proposal_row.html:37
msgid "Decline invitation"
msgstr "招待を拒否"
#: templates/proposals/_proposal_fields.html:4
msgid "Submitted by"
msgstr ""
#: templates/proposals/_proposal_fields.html:7
msgid "Track"
msgstr "トラック"
#: templates/proposals/_proposal_fields.html:10
msgid "Audience Level"
msgstr "受講者のレベル"
#: templates/proposals/_proposal_fields.html:14
msgid "Additional Speakers"
msgstr "追加の講演者"
#: templates/proposals/_proposal_fields.html:21
msgid "Invitation Sent"
msgstr "送信された招待"
#: templates/proposals/_proposal_fields.html:28
msgid "Description"
msgstr "記述"
#: templates/proposals/_proposal_fields.html:31
msgid "Abstract"
msgstr "講演概要"
#: templates/proposals/_proposal_fields.html:34
msgid "Notes"
msgstr "備考"
#: templates/proposals/_proposal_fields.html:37
msgid "Speaker Bio"
msgstr "講演者略歴"
#: templates/proposals/_proposal_fields.html:40
msgid "Documents"
msgstr "資料"
#: templates/proposals/proposal_cancel.html:7
msgid "Cancel Proposal"
msgstr "講演提案のキャンセル"
#: templates/proposals/proposal_cancel.html:16
msgid "No, keep it for now"
msgstr "いいえ、このままにします。"
#: templates/proposals/proposal_detail.html:14
msgid "Edit this proposal"
msgstr "この提案を編集"
#: templates/proposals/proposal_detail.html:17
msgid "Cancel this proposal"
msgstr "この提案をキャンセル"
#: templates/proposals/proposal_detail.html:21
msgid "Remove me from this proposal"
msgstr "この提案から自分を削除する。"
#: templates/proposals/proposal_detail.html:33
#: templates/reviews/review_detail.html:74
msgid "Proposal Details"
msgstr "提案の詳細"
#: templates/proposals/proposal_detail.html:35
#: templates/proposals/proposal_detail.html:47
msgid "Supporting Documents"
msgstr "補足資料"
#: templates/proposals/proposal_detail.html:38
msgid "Reviewer Feedback"
msgstr "レビュアーからのフィードバック"
#: templates/proposals/proposal_detail.html:57
msgid "delete"
msgstr "削除"
#: templates/proposals/proposal_detail.html:64
msgid "No supporting documents attached to this proposal."
msgstr "この提案に補足資料は添付されていません。"
#: templates/proposals/proposal_detail.html:66
msgid "Add Document"
msgstr "資料の追加"
#: templates/proposals/proposal_detail.html:73
msgid "Conversation with Reviewers"
msgstr "レビュアーとの会話記録"
#: templates/proposals/proposal_detail.html:83
msgid "Leave a Message"
msgstr "メッセージを残す"
#: templates/proposals/proposal_detail.html:85
msgid "You can leave a message for the reviewers here."
msgstr "メッセージをレビューアに残すことができます。"
#: templates/proposals/proposal_detail.html:94
msgid "Submit"
msgstr "投稿する"
#: templates/proposals/proposal_speaker_manage.html:7
msgid "Proposal:"
msgstr "講演提案:"
#: templates/proposals/proposal_speaker_manage.html:10
msgid "Edit proposal"
msgstr "講演提案の編集"
#: templates/proposals/proposal_speaker_manage.html:14
msgid "Current Speakers"
msgstr "現在登録されている講演者"
#: templates/proposals/proposal_speaker_manage.html:20
msgid "pending invitation"
msgstr "保留されている招待"
#: templates/proposals/proposal_speaker_manage.html:24
msgid "Add another speaker"
msgstr "他の講演者を追加"
#: templates/proposals/proposal_submit.html:6
msgid "Submit A Proposal"
msgstr "提案を投稿"
#: templates/reviews/_review_table.html:6
#: templates/reviews/result_notification.html:45
msgid "Speaker / Title"
msgstr "講演者/タイトル"
#: templates/reviews/_review_table.html:7
#: templates/reviews/result_notification.html:46
msgid "Category"
msgstr "カテゴリ"
#: templates/reviews/_review_table.html:9
msgid "+1"
msgstr ""
#: templates/reviews/_review_table.html:10
msgid "+0"
msgstr ""
#: templates/reviews/_review_table.html:11
msgid "-0"
msgstr ""
#: templates/reviews/_review_table.html:12
msgid "-1"
msgstr ""
#: templates/reviews/_review_table.html:13
msgid "Your Rating"
msgstr "あなたの投票"
#: templates/reviews/base.html:64
msgid "All Reviews"
msgstr "全てのレビュアー"
#: templates/reviews/base.html:77
msgid "Voting Status"
msgstr "投票の状態"
#: templates/reviews/result_notification.html:47
msgid "Status"
msgstr "状態"
#: templates/reviews/result_notification.html:48
msgid "Notified?"
msgstr "連絡済み?"
#: templates/reviews/review_detail.html:76
msgid "Speaker Feedback"
msgstr "講演者へのフィードバック"
#: templates/reviews/review_detail.html:84
msgid "Current Results"
msgstr "現在のところの結果"
#: templates/reviews/review_detail.html:91
msgid "Total Responses"
msgstr "全応答数"
#: templates/reviews/review_detail.html:108
msgid "Submit Review"
msgstr "レビューの投稿"
#: templates/reviews/review_detail.html:148
msgid "Conversation with the submitter"
msgstr "投稿者への連絡"
#: templates/reviews/review_detail.html:162
msgid "Send a message"
msgstr "メッセージ送信"
#: templates/reviews/review_detail.html:164
msgid ""
"\n"
" If you'd like to communicate with the submitter, "
"use the following form and he or she will be\n"
" notified and given the opportunity to respond.\n"
" "
msgstr ""
"\n"
" もし、投稿者と連絡したい場合は、次の投稿フォームを"
"用いて知らせることができます。\n"
" そして、回答の機会を与えることができます。\n"
" "
#: templates/schedule/_slot_edit.html:5
msgid "Edit Slot"
msgstr "スロットの編集"
#: templates/speakers/speaker_create.html:7
#: templates/speakers/speaker_create.html:14
msgid "Create Speaker Profile"
msgstr "講演者プロフィールの作成"
#: templates/speakers/speaker_edit.html:7
#: templates/speakers/speaker_edit.html:14
msgid "Edit Speaker Profile"
msgstr "講演者プロフィールの編集"
#: templates/sponsorship/add.html:7 templates/sponsorship/add.html.py:14
msgid "Add a Sponsor"
msgstr "スポンサーの追加"
#: templates/sponsorship/apply.html:7
msgid "Apply to be a Sponsor"
msgstr "スポンサーに応募する"
#: templates/sponsorship/apply.html:17
msgid "Apply to Be a Sponsor"
msgstr "スポンサーに応募する"
#: templates/sponsorship/list.html:7 templates/sponsorship/list.html.py:14
msgid "About Our Sponsors"
msgstr "スポンサーについて"

View file

@ -9,6 +9,7 @@ from django.utils.timezone import now
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
import reversion import reversion
@ -106,8 +107,15 @@ class ProposalBase(models.Model):
editable=False, editable=False,
) )
speaker = models.ForeignKey(Speaker, related_name="proposals") speaker = models.ForeignKey(Speaker, related_name="proposals")
def additional_speaker_validator(self, a_speaker):
if a_speaker.speaker.email == self.speaker.email:
raise ValidationError(_("%s is same as primary speaker.") % a_speaker.speaker.email)
if a_speaker in [self.additional_speakers]:
raise ValidationError(_("%s has already been in speakers.") % a_speaker.speaker.email)
additional_speakers = models.ManyToManyField(Speaker, through="AdditionalSpeaker", additional_speakers = models.ManyToManyField(Speaker, through="AdditionalSpeaker",
blank=True) blank=True, validators=[additional_speaker_validator])
cancelled = models.BooleanField(default=False) cancelled = models.BooleanField(default=False)
def can_edit(self): def can_edit(self):
@ -147,7 +155,6 @@ class ProposalBase(models.Model):
"kind": self.kind.name, "kind": self.kind.name,
} }
reversion.register(ProposalBase) reversion.register(ProposalBase)
@ -170,6 +177,14 @@ class AdditionalSpeaker(models.Model):
class Meta: class Meta:
unique_together = ("speaker", "proposalbase") unique_together = ("speaker", "proposalbase")
def __unicode__(self):
if self.status is self.SPEAKING_STATUS_PENDING:
return _(u"pending speaker (%s)") % self.speaker.email
elif self.status is self.SPEAKING_STATUS_DECLINED:
return _(u"declined speaker (%s)") % self.speaker.email
else:
return self.speaker.name
def uuid_filename(instance, filename): def uuid_filename(instance, filename):
ext = filename.split(".")[-1] ext = filename.split(".")[-1]

View file

@ -3,28 +3,53 @@ from django.contrib import admin
from symposion.schedule.models import Schedule, Day, Room, SlotKind, Slot, SlotRoom, Presentation, Session, SessionRole from symposion.schedule.models import Schedule, Day, Room, SlotKind, Slot, SlotRoom, Presentation, Session, SessionRole
admin.site.register(Schedule) class DayInline(admin.StackedInline):
model = Day
extra = 2
class SlotKindInline(admin.StackedInline):
model = SlotKind
class ScheduleAdmin(admin.ModelAdmin):
model = Schedule
inlines = [DayInline, SlotKindInline, ]
class SlotRoomInline(admin.TabularInline):
model = SlotRoom
extra = 1
class SlotAdmin(admin.ModelAdmin):
list_filter = ("day", "kind")
list_display = ("day", "start", "end", "kind", "content")
inlines = [SlotRoomInline]
class RoomAdmin(admin.ModelAdmin):
list_display = ["name", "order", "schedule"]
list_filter = ["schedule"]
inlines = [SlotRoomInline]
class PresentationAdmin(admin.ModelAdmin):
model = Presentation
list_filter = ("section", "cancelled", "slot")
admin.site.register(Day) admin.site.register(Day)
admin.site.register(
Room,
list_display=("name", "order", "schedule"),
list_filter=("schedule",)
)
admin.site.register( admin.site.register(
SlotKind, SlotKind,
list_display=("label", "schedule"), list_display=["label", "schedule"],
)
admin.site.register(
Slot,
list_display=("day", "start", "end", "kind", "content")
) )
admin.site.register( admin.site.register(
SlotRoom, SlotRoom,
list_display=("slot", "room") list_display=["slot", "room"]
) )
admin.site.register(Schedule, ScheduleAdmin)
admin.site.register(Room, RoomAdmin)
admin.site.register(Slot, SlotAdmin)
admin.site.register(Session) admin.site.register(Session)
admin.site.register(SessionRole) admin.site.register(SessionRole)
admin.site.register( admin.site.register(Presentation, PresentationAdmin)
Presentation,
list_filter=("section", "cancelled", "slot")
)

View file

@ -125,7 +125,8 @@ class Slot(models.Model):
return Room.objects.filter(pk__in=self.slotroom_set.values("room")) return Room.objects.filter(pk__in=self.slotroom_set.values("room"))
def __unicode__(self): def __unicode__(self):
return u"%s %s (%s - %s)" % (self.day, self.kind, self.start, self.end) roomlist = ' '.join(map(lambda r: r.__unicode__(), self.rooms))
return u"%s %s (%s - %s) %s" % (self.day, self.kind, self.start, self.end, roomlist)
class Meta: class Meta:
ordering = ["day", "start", "end"] ordering = ["day", "start", "end"]

View file

@ -1,4 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from symposion.sponsorship.models import SponsorLevel, Sponsor, Benefit, BenefitLevel, \ from symposion.sponsorship.models import SponsorLevel, Sponsor, Benefit, BenefitLevel, \
SponsorBenefit SponsorBenefit
@ -43,7 +46,16 @@ class SponsorAdmin(admin.ModelAdmin):
}) })
] ]
inlines = [SponsorBenefitInline] inlines = [SponsorBenefitInline]
list_display = ["name", "external_url", "level", "active"] list_display = ["name", "external_url", "level", "active", "contact", "applicant_field"]
def contact(self, sponsor):
return mark_safe('<a href="mailto:%s">%s</a>' % (escape(sponsor.contact_email), escape(sponsor.contact_name)))
def applicant_field(self, sponsor):
name = sponsor.applicant.get_full_name()
email = sponsor.applicant.email
return mark_safe('<a href="mailto:%s">%s</a>' % (escape(email), escape(name)))
applicant_field.short_description = _(u"Applicant")
def get_form(self, *args, **kwargs): def get_form(self, *args, **kwargs):
# @@@ kinda ugly but using choices= on NullBooleanField is broken # @@@ kinda ugly but using choices= on NullBooleanField is broken

View file

@ -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"))

View file

@ -1,5 +1,6 @@
import datetime import datetime
from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
@ -39,6 +40,7 @@ class Sponsor(models.Model):
null=True) null=True)
name = models.CharField(_("Sponsor Name"), max_length=100) name = models.CharField(_("Sponsor Name"), max_length=100)
display_url = models.URLField(_("display URL"), blank=True)
external_url = models.URLField(_("external URL")) external_url = models.URLField(_("external URL"))
annotation = models.TextField(_("annotation"), blank=True) annotation = models.TextField(_("annotation"), blank=True)
contact_name = models.CharField(_("Contact Name"), max_length=100) contact_name = models.CharField(_("Contact Name"), max_length=100)
@ -65,6 +67,12 @@ class Sponsor(models.Model):
return reverse("sponsor_detail", kwargs={"pk": self.pk}) return reverse("sponsor_detail", kwargs={"pk": self.pk})
return reverse("sponsor_list") return reverse("sponsor_list")
def get_display_url(self):
if self.display_url:
return self.display_url
else:
return self.external_url
@property @property
def website_logo(self): def website_logo(self):
if self.sponsor_logo is None: if self.sponsor_logo is None:
@ -144,9 +152,17 @@ post_save.connect(_check_level_change, sender=Sponsor)
BENEFIT_TYPE_CHOICES = [ BENEFIT_TYPE_CHOICES = [
("text", "Text"), ("text", "Text"),
("richtext", "Rich Text"),
("file", "File"), ("file", "File"),
("weblogo", "Web Logo"), ("weblogo", "Web Logo"),
("simple", "Simple") ("simple", "Simple"),
("option", "Option")
]
CONTENT_TYPE_CHOICES = [
("simple", "Simple"),
] + [
("listing_text_%s" % lang, "Listing Text (%s)" % label) for lang, label in settings.LANGUAGES
] ]
@ -154,8 +170,10 @@ class Benefit(models.Model):
name = models.CharField(_("name"), max_length=100) name = models.CharField(_("name"), max_length=100)
description = models.TextField(_("description"), blank=True) description = models.TextField(_("description"), blank=True)
type = models.CharField(_("type"), choices=BENEFIT_TYPE_CHOICES, max_length=10, type = models.CharField(_("type"), choices=BENEFIT_TYPE_CHOICES,
default="simple") max_length=10, default="simple")
content_type = models.CharField(_("content type"), choices=CONTENT_TYPE_CHOICES,
max_length=20, default="simple")
def __unicode__(self): def __unicode__(self):
return self.name return self.name

View file

@ -1,4 +1,5 @@
from django import template from django import template
from django.template.defaultfilters import linebreaks, urlize
from symposion.conference.models import current_conference from symposion.conference.models import current_conference
from symposion.sponsorship.models import Sponsor, SponsorLevel from symposion.sponsorship.models import Sponsor, SponsorLevel
@ -75,3 +76,45 @@ def sponsor_levels(parser, token):
{% sponsor_levels as levels %} {% sponsor_levels as levels %}
""" """
return SponsorLevelNode.handle_token(parser, token) return SponsorLevelNode.handle_token(parser, token)
class LocalizedTextNode(template.Node):
@classmethod
def handle_token(cls, parser, token):
bits = token.split_contents()
if len(bits) == 3:
return cls(bits[2], bits[1][1:-1])
elif len(bits) == 5 and bits[-2] == "as":
return cls(bits[2], bits[1][1:-1], bits[4])
else:
raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
def __init__(self, sponsor, content_type, context_var=None):
self.sponsor_var = template.Variable(sponsor)
self.content_type = content_type
self.content_var = context_var
def render(self, context):
s = ''
try:
sponsor = self.sponsor_var.resolve(context)
content_type = '%s_%s' % (self.content_type, context['request'].LANGUAGE_CODE)
texts = sponsor.sponsor_benefits.filter(benefit__content_type=content_type)
if texts.count() > 0:
s = linebreaks(urlize(texts[0].text, autoescape=True))
if self.content_var:
context[self.content_var] = s
s = ''
except:
pass
return s
@register.tag
def localized_text(parser, token):
"""
{% localized_text "content_type" sponsor %}
{% localized_text "content_type" sponsor as localized_text %}
"""
return LocalizedTextNode.handle_token(parser, token)

View file

@ -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("""<body class="login">""", 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("""<body class="login">""", 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}/<sponsor_level>/<sponsor_name>/<filename>
# 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")

View file

@ -7,5 +7,6 @@ urlpatterns = patterns(
url(r"^$", TemplateView.as_view(template_name="sponsorship/list.html"), name="sponsor_list"), url(r"^$", TemplateView.as_view(template_name="sponsorship/list.html"), name="sponsor_list"),
url(r"^apply/$", "sponsor_apply", name="sponsor_apply"), url(r"^apply/$", "sponsor_apply", name="sponsor_apply"),
url(r"^add/$", "sponsor_add", name="sponsor_add"), url(r"^add/$", "sponsor_add", name="sponsor_add"),
url(r"^ziplogos/$", "sponsor_zip_logo_files", name="sponsor_zip_logos"),
url(r"^(?P<pk>\d+)/$", "sponsor_detail", name="sponsor_detail"), url(r"^(?P<pk>\d+)/$", "sponsor_detail", name="sponsor_detail"),
) )

View file

@ -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.shortcuts import render_to_response, redirect, get_object_or_404
from django.template import RequestContext from django.template import RequestContext
from django.utils.translation import ugettext_lazy as _
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.views.decorators import staff_member_required
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from symposion.sponsorship.forms import SponsorApplicationForm, SponsorDetailsForm, \ from symposion.sponsorship.forms import SponsorApplicationForm, \
SponsorBenefitsFormSet SponsorDetailsForm, SponsorBenefitsFormSet
from symposion.sponsorship.models import Sponsor, SponsorBenefit from symposion.sponsorship.models import Benefit, Sponsor, SponsorBenefit, \
SponsorLevel
log = logging.getLogger(__name__)
@login_required @login_required
@ -18,13 +33,13 @@ def sponsor_apply(request):
sponsor = form.save() sponsor = form.save()
if sponsor.sponsor_benefits.all(): if sponsor.sponsor_benefits.all():
# Redirect user to sponsor_detail to give extra information. # Redirect user to sponsor_detail to give extra information.
messages.success(request, "Thank you for your sponsorship " messages.success(request, _("Thank you for your sponsorship "
"application. Please update your " "application. Please update your "
"benefit details below.") "benefit details below."))
return redirect("sponsor_detail", pk=sponsor.pk) return redirect("sponsor_detail", pk=sponsor.pk)
else: else:
messages.success(request, "Thank you for your sponsorship " messages.success(request, _("Thank you for your sponsorship "
"application.") "application."))
return redirect("dashboard") return redirect("dashboard")
else: else:
form = SponsorApplicationForm(user=request.user) form = SponsorApplicationForm(user=request.user)
@ -87,3 +102,96 @@ def sponsor_detail(request, pk):
"form": form, "form": form,
"formset": formset, "formset": formset,
}, context_instance=RequestContext(request)) }, 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

View file

@ -1,384 +0,0 @@
/* @group Base */
.chzn-container {
font-size: 13px;
position: relative;
display: inline-block;
zoom: 1;
*display: inline;
}
.chzn-container .chzn-drop {
background: #fff;
border: 1px solid #aaa;
border-top: 0;
position: absolute;
top: 29px;
left: 0;
-webkit-box-shadow: 0 4px 5px rgba(0,0,0,.15);
-moz-box-shadow : 0 4px 5px rgba(0,0,0,.15);
box-shadow : 0 4px 5px rgba(0,0,0,.15);
z-index: 1010;
}
/* @end */
/* @group Single Chosen */
.chzn-container-single .chzn-single {
background-color: #ffffff;
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0 );
background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #ffffff), color-stop(50%, #f6f6f6), color-stop(52%, #eeeeee), color-stop(100%, #f4f4f4));
background-image: -webkit-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background-image: -moz-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background-image: -o-linear-gradient(top, #ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
background-image: linear-gradient(#ffffff 20%, #f6f6f6 50%, #eeeeee 52%, #f4f4f4 100%);
-webkit-border-radius: 5px;
-moz-border-radius : 5px;
border-radius : 5px;
-moz-background-clip : padding;
-webkit-background-clip: padding-box;
background-clip : padding-box;
border: 1px solid #aaaaaa;
-webkit-box-shadow: 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
-moz-box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
box-shadow : 0 0 3px #ffffff inset, 0 1px 1px rgba(0,0,0,0.1);
display: block;
overflow: hidden;
white-space: nowrap;
position: relative;
height: 23px;
line-height: 24px;
padding: 0 0 0 8px;
color: #444444;
text-decoration: none;
}
.chzn-container-single .chzn-default {
color: #999;
}
.chzn-container-single .chzn-single span {
margin-right: 26px;
display: block;
overflow: hidden;
white-space: nowrap;
-o-text-overflow: ellipsis;
-ms-text-overflow: ellipsis;
text-overflow: ellipsis;
}
.chzn-container-single .chzn-single abbr {
display: block;
position: absolute;
right: 26px;
top: 6px;
width: 12px;
height: 13px;
font-size: 1px;
background: url('chosen-sprite.png') right top no-repeat;
}
.chzn-container-single .chzn-single abbr:hover {
background-position: right -11px;
}
.chzn-container-single.chzn-disabled .chzn-single abbr:hover {
background-position: right top;
}
.chzn-container-single .chzn-single div {
position: absolute;
right: 0;
top: 0;
display: block;
height: 100%;
width: 18px;
}
.chzn-container-single .chzn-single div b {
background: url('chosen-sprite.png') no-repeat 0 0;
display: block;
width: 100%;
height: 100%;
}
.chzn-container-single .chzn-search {
padding: 3px 4px;
position: relative;
margin: 0;
white-space: nowrap;
z-index: 1010;
}
.chzn-container-single .chzn-search input {
background: #fff url('chosen-sprite.png') no-repeat 100% -22px;
background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
background: url('chosen-sprite.png') no-repeat 100% -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background: url('chosen-sprite.png') no-repeat 100% -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background: url('chosen-sprite.png') no-repeat 100% -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background: url('chosen-sprite.png') no-repeat 100% -22px, linear-gradient(#eeeeee 1%, #ffffff 15%);
margin: 1px 0;
padding: 4px 20px 4px 5px;
outline: 0;
border: 1px solid #aaa;
font-family: sans-serif;
font-size: 1em;
}
.chzn-container-single .chzn-drop {
-webkit-border-radius: 0 0 4px 4px;
-moz-border-radius : 0 0 4px 4px;
border-radius : 0 0 4px 4px;
-moz-background-clip : padding;
-webkit-background-clip: padding-box;
background-clip : padding-box;
}
/* @end */
.chzn-container-single-nosearch .chzn-search input {
position: absolute;
left: -9000px;
}
/* @group Multi Chosen */
.chzn-container-multi .chzn-choices {
background-color: #fff;
background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
background-image: -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background-image: linear-gradient(#eeeeee 1%, #ffffff 15%);
border: 1px solid #aaa;
margin: 0;
padding: 0;
cursor: text;
overflow: hidden;
height: auto !important;
height: 1%;
position: relative;
}
.chzn-container-multi .chzn-choices li {
float: left;
list-style: none;
}
.chzn-container-multi .chzn-choices .search-field {
white-space: nowrap;
margin: 0;
padding: 0;
}
.chzn-container-multi .chzn-choices .search-field input {
color: #666;
background: transparent !important;
border: 0 !important;
font-family: sans-serif;
font-size: 100%;
height: 15px;
padding: 5px;
margin: 1px 0;
outline: 0;
-webkit-box-shadow: none;
-moz-box-shadow : none;
box-shadow : none;
}
.chzn-container-multi .chzn-choices .search-field .default {
color: #999;
}
.chzn-container-multi .chzn-choices .search-choice {
-webkit-border-radius: 3px;
-moz-border-radius : 3px;
border-radius : 3px;
-moz-background-clip : padding;
-webkit-background-clip: padding-box;
background-clip : padding-box;
background-color: #e4e4e4;
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#f4f4f4', endColorstr='#eeeeee', GradientType=0 );
background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eeeeee));
background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: -o-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
background-image: linear-gradient(#f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eeeeee 100%);
-webkit-box-shadow: 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
-moz-box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
box-shadow : 0 0 2px #ffffff inset, 0 1px 0 rgba(0,0,0,0.05);
color: #333;
border: 1px solid #aaaaaa;
line-height: 13px;
padding: 3px 20px 3px 5px;
margin: 3px 0 3px 5px;
position: relative;
cursor: default;
}
.chzn-container-multi .chzn-choices .search-choice-focus {
background: #d4d4d4;
}
.chzn-container-multi .chzn-choices .search-choice .search-choice-close {
display: block;
position: absolute;
right: 3px;
top: 4px;
width: 12px;
height: 13px;
font-size: 1px;
background: url('chosen-sprite.png') right top no-repeat;
}
.chzn-container-multi .chzn-choices .search-choice .search-choice-close:hover {
background-position: right -11px;
}
.chzn-container-multi .chzn-choices .search-choice-focus .search-choice-close {
background-position: right -11px;
}
/* @end */
/* @group Results */
.chzn-container .chzn-results {
margin: 0 4px 4px 0;
max-height: 240px;
padding: 0 0 0 4px;
position: relative;
overflow-x: hidden;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
.chzn-container-multi .chzn-results {
margin: -1px 0 0;
padding: 0;
}
.chzn-container .chzn-results li {
display: none;
line-height: 15px;
padding: 5px 6px;
margin: 0;
list-style: none;
}
.chzn-container .chzn-results .active-result {
cursor: pointer;
display: list-item;
}
.chzn-container .chzn-results .highlighted {
background-color: #3875d7;
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#3875d7', endColorstr='#2a62bc', GradientType=0 );
background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #3875d7), color-stop(90%, #2a62bc));
background-image: -webkit-linear-gradient(top, #3875d7 20%, #2a62bc 90%);
background-image: -moz-linear-gradient(top, #3875d7 20%, #2a62bc 90%);
background-image: -o-linear-gradient(top, #3875d7 20%, #2a62bc 90%);
background-image: linear-gradient(#3875d7 20%, #2a62bc 90%);
color: #fff;
}
.chzn-container .chzn-results li em {
background: #feffde;
font-style: normal;
}
.chzn-container .chzn-results .highlighted em {
background: transparent;
}
.chzn-container .chzn-results .no-results {
background: #f4f4f4;
display: list-item;
}
.chzn-container .chzn-results .group-result {
cursor: default;
color: #999;
font-weight: bold;
}
.chzn-container .chzn-results .group-option {
padding-left: 15px;
}
.chzn-container-multi .chzn-drop .result-selected {
display: none;
}
.chzn-container .chzn-results-scroll {
background: white;
margin: 0 4px;
position: absolute;
text-align: center;
width: 321px; /* This should by dynamic with js */
z-index: 1;
}
.chzn-container .chzn-results-scroll span {
display: inline-block;
height: 17px;
text-indent: -5000px;
width: 9px;
}
.chzn-container .chzn-results-scroll-down {
bottom: 0;
}
.chzn-container .chzn-results-scroll-down span {
background: url('chosen-sprite.png') no-repeat -4px -3px;
}
.chzn-container .chzn-results-scroll-up span {
background: url('chosen-sprite.png') no-repeat -22px -3px;
}
/* @end */
/* @group Active */
.chzn-container-active .chzn-single {
-webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
-moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
box-shadow : 0 0 5px rgba(0,0,0,.3);
border: 1px solid #5897fb;
}
.chzn-container-active .chzn-single-with-drop {
border: 1px solid #aaa;
-webkit-box-shadow: 0 1px 0 #fff inset;
-moz-box-shadow : 0 1px 0 #fff inset;
box-shadow : 0 1px 0 #fff inset;
background-color: #eee;
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0 );
background-image: -webkit-gradient(linear, 0 0, 0 100%, color-stop(20%, #eeeeee), color-stop(80%, #ffffff));
background-image: -webkit-linear-gradient(top, #eeeeee 20%, #ffffff 80%);
background-image: -moz-linear-gradient(top, #eeeeee 20%, #ffffff 80%);
background-image: -o-linear-gradient(top, #eeeeee 20%, #ffffff 80%);
background-image: linear-gradient(#eeeeee 20%, #ffffff 80%);
-webkit-border-bottom-left-radius : 0;
-webkit-border-bottom-right-radius: 0;
-moz-border-radius-bottomleft : 0;
-moz-border-radius-bottomright: 0;
border-bottom-left-radius : 0;
border-bottom-right-radius: 0;
}
.chzn-container-active .chzn-single-with-drop div {
background: transparent;
border-left: none;
}
.chzn-container-active .chzn-single-with-drop div b {
background-position: -18px 1px;
}
.chzn-container-active .chzn-choices {
-webkit-box-shadow: 0 0 5px rgba(0,0,0,.3);
-moz-box-shadow : 0 0 5px rgba(0,0,0,.3);
box-shadow : 0 0 5px rgba(0,0,0,.3);
border: 1px solid #5897fb;
}
.chzn-container-active .chzn-choices .search-field input {
color: #111 !important;
}
/* @end */
/* @group Disabled Support */
.chzn-disabled {
cursor: default;
opacity:0.5 !important;
}
.chzn-disabled .chzn-single {
cursor: default;
}
.chzn-disabled .chzn-choices .search-choice .search-choice-close {
cursor: default;
}
/* @group Right to Left */
.chzn-rtl { text-align: right; }
.chzn-rtl .chzn-single { padding: 0 8px 0 0; overflow: visible; }
.chzn-rtl .chzn-single span { margin-left: 26px; margin-right: 0; direction: rtl; }
.chzn-rtl .chzn-single div { left: 3px; right: auto; }
.chzn-rtl .chzn-single abbr {
left: 26px;
right: auto;
}
.chzn-rtl .chzn-choices .search-field input { direction: rtl; }
.chzn-rtl .chzn-choices li { float: right; }
.chzn-rtl .chzn-choices .search-choice { padding: 3px 5px 3px 19px; margin: 3px 5px 3px 0; }
.chzn-rtl .chzn-choices .search-choice .search-choice-close { left: 4px; right: auto; background-position: right top;}
.chzn-rtl.chzn-container-single .chzn-results { margin: 0 0 4px 4px; padding: 0 4px 0 0; }
.chzn-rtl .chzn-results .group-option { padding-left: 0; padding-right: 15px; }
.chzn-rtl.chzn-container-active .chzn-single-with-drop div { border-right: none; }
.chzn-rtl .chzn-search input {
background: #fff url('chosen-sprite.png') no-repeat -38px -22px;
background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-gradient(linear, 0 0, 0 100%, color-stop(1%, #eeeeee), color-stop(15%, #ffffff));
background: url('chosen-sprite.png') no-repeat -38px -22px, -webkit-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background: url('chosen-sprite.png') no-repeat -38px -22px, -moz-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background: url('chosen-sprite.png') no-repeat -38px -22px, -o-linear-gradient(top, #eeeeee 1%, #ffffff 15%);
background: url('chosen-sprite.png') no-repeat -38px -22px, linear-gradient(#eeeeee 1%, #ffffff 15%);
padding: 4px 5px 4px 20px;
direction: rtl;
}
/* @end */

File diff suppressed because one or more lines are too long