Pull in the vendorized Symposion

We're lock step with this, we're installing with master.
Upstream is dead.

We can't roll back.
It doesn't make since to pin to every commit and revision our apps
version and push it.

We're just going to pull this in to gain full lockstep and call it good.
This commit is contained in:
Sachi King 2017-05-27 20:12:48 +10:00
commit 2ad28ebf71
127 changed files with 11159 additions and 0 deletions

27
vendor/symposion/LICENSE vendored Normal file
View file

@ -0,0 +1,27 @@
Copyright (c) 2010-2014, Eldarion, Inc. and contributors
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Eldarion, Inc. nor the names of its contributors may
be used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

1
vendor/symposion/__init__.py vendored Normal file
View file

@ -0,0 +1 @@
__version__ = "1.0b2.dev3"

8
vendor/symposion/conf.py vendored Normal file
View file

@ -0,0 +1,8 @@
from django.conf import settings # noqa
from appconf import AppConf
class SymposionAppConf(AppConf):
VOTE_THRESHOLD = 3

View file

@ -0,0 +1 @@
default_app_config = "symposion.conference.apps.ConferenceConfig"

22
vendor/symposion/conference/admin.py vendored Normal file
View file

@ -0,0 +1,22 @@
from django.contrib import admin
from symposion.conference.models import Conference, Section
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(
Section,
prepopulated_fields={"slug": ("name",)},
list_display=("name", "conference", "start_date", "end_date")
)

8
vendor/symposion/conference/apps.py vendored Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class ConferenceConfig(AppConfig):
name = "symposion.conference"
label = "symposion_conference"
verbose_name = _("Symposion Conference")

View file

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-17 03:34
from django.db import migrations, models
import django.db.models.deletion
import timezone_field.fields
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Conference',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='Title')),
('start_date', models.DateField(blank=True, null=True, verbose_name='Start date')),
('end_date', models.DateField(blank=True, null=True, verbose_name='End date')),
('timezone', timezone_field.fields.TimeZoneField(blank=True, verbose_name='timezone')),
],
options={
'verbose_name': 'conference',
'verbose_name_plural': 'conferences',
},
),
migrations.CreateModel(
name='Section',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Name')),
('slug', models.SlugField(verbose_name='Slug')),
('start_date', models.DateField(blank=True, null=True, verbose_name='Start date')),
('end_date', models.DateField(blank=True, null=True, verbose_name='End date')),
('conference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_conference.Conference', verbose_name='Conference')),
],
options={
'ordering': ['start_date'],
'verbose_name': 'section',
'verbose_name_plural': 'sections',
},
),
]

View file

82
vendor/symposion/conference/models.py vendored Normal file
View file

@ -0,0 +1,82 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from timezone_field import TimeZoneField
CONFERENCE_CACHE = {}
class Conference(models.Model):
"""
the full conference for a specific year, e.g. US PyCon 2012.
"""
title = models.CharField(_("Title"), max_length=100)
# when the conference runs
start_date = models.DateField(_("Start date"), null=True, blank=True)
end_date = models.DateField(_("End date"), null=True, blank=True)
# timezone the conference is in
timezone = TimeZoneField(blank=True, verbose_name=_("timezone"))
def __str__(self):
return self.title
def save(self, *args, **kwargs):
super(Conference, self).save(*args, **kwargs)
if self.id in CONFERENCE_CACHE:
del CONFERENCE_CACHE[self.id]
def delete(self):
pk = self.pk
super(Conference, self).delete()
try:
del CONFERENCE_CACHE[pk]
except KeyError:
pass
class Meta(object):
verbose_name = _("conference")
verbose_name_plural = _("conferences")
class Section(models.Model):
"""
a section of the conference such as "Tutorials", "Workshops",
"Talks", "Expo", "Sprints", that may have its own review and
scheduling process.
"""
conference = models.ForeignKey(Conference, verbose_name=_("Conference"))
name = models.CharField(_("Name"), max_length=100)
slug = models.SlugField(verbose_name=_("Slug"))
# when the section runs
start_date = models.DateField(_("Start date"), null=True, blank=True)
end_date = models.DateField(_("End date"), null=True, blank=True)
def __str__(self):
return "%s %s" % (self.conference, self.name)
class Meta(object):
verbose_name = _("section")
verbose_name_plural = _("sections")
ordering = ["start_date"]
def current_conference():
from django.conf import settings
try:
conf_id = settings.CONFERENCE_ID
except AttributeError:
from django.core.exceptions import ImproperlyConfigured
raise ImproperlyConfigured("You must set the CONFERENCE_ID setting.")
try:
current_conf = CONFERENCE_CACHE[conf_id]
except KeyError:
current_conf = Conference.objects.get(pk=conf_id)
CONFERENCE_CACHE[conf_id] = current_conf
return current_conf

7
vendor/symposion/conference/urls.py vendored Normal file
View file

@ -0,0 +1,7 @@
from django.conf.urls import url
from .views import user_list
urlpatterns = [
url(r"^users/$", user_list, name="user_list"),
]

16
vendor/symposion/conference/views.py vendored Normal file
View file

@ -0,0 +1,16 @@
from django.http import Http404
from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
@login_required
def user_list(request):
if not request.user.is_staff:
raise Http404()
return render(request, "symposion/conference/user_list.html", {
"users": User.objects.all(),
})

5
vendor/symposion/constants.py vendored Normal file
View file

@ -0,0 +1,5 @@
TEXT_FIELD_MONOSPACE_NOTE=(
"This field is rendered with the monospace font "
"<a href=\"https://sourcefoundry.org/hack/\">Hack</a> with "
"whitespace preserved")

Binary file not shown.

View file

@ -0,0 +1,537 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-07-31 14:47-0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\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 ""
#: 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 ""
#: 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 ""
#: 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 ""
#: sponsorship/models.py:43
msgid "annotation"
msgstr ""
#: sponsorship/models.py:44
msgid "Contact Name"
msgstr ""
#: sponsorship/models.py:45
msgid "Contact Email"
msgstr ""
#: 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 ""
#: templates/emails/teams_user_applied/subject.txt:1
#, python-format
msgid "%(username)s has applied to to join \"%(team)s\""
msgstr ""
#: 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 ""
#: templates/emails/teams_user_invited/subject.txt:1
#, python-format
msgid "You have been invited to join \"%(team)s\""
msgstr ""
#: 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 ""
#: 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 ""

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 "スポンサーについて"

0
vendor/symposion/models.py vendored Normal file
View file

View file

@ -0,0 +1 @@
default_app_config = "symposion.proposals.apps.ProposalsConfig"

38
vendor/symposion/proposals/actions.py vendored Normal file
View file

@ -0,0 +1,38 @@
import csv
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
def export_as_csv_action(description=None, fields=None, exclude=None,
header=True):
"""
This function returns an export csv action
'fields' and 'exclude' work like in Django ModelForm
'header' is whether or not to output the column names as the first row
"""
def export_as_csv(modeladmin, request, queryset):
"""
Generic csv export admin action.
based on http://djangosnippets.org/snippets/1697/
"""
opts = modeladmin.model._meta
if fields:
fieldset = set(fields)
field_names = fieldset
elif exclude:
excludeset = set(exclude)
field_names = field_names - excludeset
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = "attachment; filename=%s.csv" % str(opts).replace(".", "_")
writer = csv.writer(response)
if header:
writer.writerow(list(field_names))
for obj in queryset:
writer.writerow(
[str(getattr(obj, field)).encode("utf-8", "replace") for field in field_names])
return response
if description is None:
description = _("Export selected objects as CSV file")
export_as_csv.short_description = description
return export_as_csv

32
vendor/symposion/proposals/admin.py vendored Normal file
View file

@ -0,0 +1,32 @@
from django.contrib import admin
# from symposion.proposals.actions import export_as_csv_action
from symposion.proposals.models import ProposalSection, ProposalKind
# admin.site.register(Proposal,
# list_display = [
# "id",
# "title",
# "speaker",
# "speaker_email",
# "kind",
# "audience_level",
# "cancelled",
# ],
# list_filter = [
# "kind__name",
# "result__accepted",
# ],
# actions = [export_as_csv_action("CSV Export", fields=[
# "id",
# "title",
# "speaker",
# "speaker_email",
# "kind",
# ])]
# )
admin.site.register(ProposalSection)
admin.site.register(ProposalKind)

8
vendor/symposion/proposals/apps.py vendored Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class ProposalsConfig(AppConfig):
name = "symposion.proposals"
label = "symposion_proposals"
verbose_name = _("Symposion Proposals")

45
vendor/symposion/proposals/forms.py vendored Normal file
View file

@ -0,0 +1,45 @@
from django import forms
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from symposion.proposals.models import SupportingDocument
# @@@ generic proposal form
class AddSpeakerForm(forms.Form):
required_css_class = 'label-required'
email = forms.EmailField(
label=_("Email address of new speaker (use their email address, not yours)")
)
def __init__(self, *args, **kwargs):
self.proposal = kwargs.pop("proposal")
super(AddSpeakerForm, self).__init__(*args, **kwargs)
def clean_email(self):
value = self.cleaned_data["email"]
exists = self.proposal.additional_speakers.filter(
Q(user=None, invite_email=value) |
Q(user__email=value)
).exists()
if exists:
raise forms.ValidationError(
_("This email address has already been invited to your talk proposal")
)
return value
class SupportingDocumentCreateForm(forms.ModelForm):
required_css_class = 'label-required'
class Meta:
model = SupportingDocument
fields = [
"file",
"description",
]

View file

@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-17 03:35
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import symposion.proposals.models
class Migration(migrations.Migration):
initial = True
dependencies = [
('symposion_speakers', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('symposion_conference', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='AdditionalSpeaker',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.IntegerField(choices=[(1, 'Pending'), (2, 'Accepted'), (3, 'Declined')], default=1, verbose_name='Status')),
],
options={
'verbose_name': 'Addtional speaker',
'verbose_name_plural': 'Additional speakers',
},
),
migrations.CreateModel(
name='ProposalBase',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='Title')),
('abstract', models.TextField(help_text="This will appear in the conference programme. Up to about 500 words. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>.", verbose_name='Abstract')),
('abstract_html', models.TextField(blank=True)),
('private_abstract', models.TextField(help_text="This will only be shown to organisers and reviewers. You should provide any details about your proposal that you don't want to be public here. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>.", verbose_name='Private Abstract')),
('private_abstract_html', models.TextField(blank=True)),
('technical_requirements', models.TextField(blank=True, help_text="Speakers will be provided with: Internet access, power, projector, audio. If you require anything in addition, please list your technical requirements here. Such as: a static IP address, A/V equipment or will be demonstrating security-related techniques on the conference network. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>.", verbose_name='Special Requirements')),
('technical_requirements_html', models.TextField(blank=True)),
('project', models.CharField(blank=True, help_text='The name of the project you will be talking about.', max_length=100)),
('project_url', models.URLField(blank=True, help_text='If your project has a webpage, specify the URL here so the committee can find out more about your proposal.', verbose_name='Project URL')),
('video_url', models.URLField(blank=True, help_text="You may optionally provide us with a link to a video of you speaking at another event, or of a short 'elevator pitch' of your proposed talk.", verbose_name='Video')),
('submitted', models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Submitted')),
('cancelled', models.BooleanField(default=False, verbose_name='Cancelled')),
('additional_speakers', models.ManyToManyField(blank=True, through='symposion_proposals.AdditionalSpeaker', to='symposion_speakers.Speaker', verbose_name='Addtional speakers')),
],
),
migrations.CreateModel(
name='ProposalKind',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Name')),
('slug', models.SlugField(verbose_name='Slug')),
('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proposal_kinds', to='symposion_conference.Section', verbose_name='Section')),
],
),
migrations.CreateModel(
name='ProposalSection',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start', models.DateTimeField(blank=True, null=True, verbose_name='Start')),
('end', models.DateTimeField(blank=True, null=True, verbose_name='End')),
('closed', models.NullBooleanField(verbose_name='Closed')),
('published', models.NullBooleanField(verbose_name='Published')),
('section', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='symposion_conference.Section', verbose_name='Section')),
],
),
migrations.CreateModel(
name='SupportingDocument',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='Created at')),
('file', models.FileField(upload_to=symposion.proposals.models.uuid_filename, verbose_name='File')),
('description', models.CharField(max_length=140, verbose_name='Description')),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='supporting_documents', to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
('uploaded_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Uploaded by')),
],
),
migrations.AddField(
model_name='proposalbase',
name='kind',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_proposals.ProposalKind', verbose_name='Kind'),
),
migrations.AddField(
model_name='proposalbase',
name='speaker',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proposals', to='symposion_speakers.Speaker', verbose_name='Speaker'),
),
migrations.AddField(
model_name='additionalspeaker',
name='proposalbase',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_proposals.ProposalBase', verbose_name='Proposalbase'),
),
migrations.AddField(
model_name='additionalspeaker',
name='speaker',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_speakers.Speaker', verbose_name='Speaker'),
),
migrations.AlterUniqueTogether(
name='additionalspeaker',
unique_together=set([('speaker', 'proposalbase')]),
),
]

View file

251
vendor/symposion/proposals/models.py vendored Normal file
View file

@ -0,0 +1,251 @@
import os
import uuid
from django.core.urlresolvers import reverse
from django.db import models
from django.db.models import Q
from django.utils.translation import ugettext_lazy as _
from django.utils.timezone import now
from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ValidationError
from model_utils.managers import InheritanceManager
from reversion import revisions as reversion
from symposion import constants
from symposion.text_parser import parse
from symposion.conference.models import Section
from symposion.speakers.models import Speaker
class ProposalSection(models.Model):
"""
configuration of proposal submissions for a specific Section.
a section is available for proposals iff:
* it is after start (if there is one) and
* it is before end (if there is one) and
* closed is NULL or False
"""
section = models.OneToOneField(Section, verbose_name=_("Section"))
start = models.DateTimeField(null=True, blank=True, verbose_name=_("Start"))
end = models.DateTimeField(null=True, blank=True, verbose_name=_("End"))
closed = models.NullBooleanField(verbose_name=_("Closed"))
published = models.NullBooleanField(verbose_name=_("Published"))
@classmethod
def available(cls):
return cls._default_manager.filter(
Q(start__lt=now()) | Q(start=None),
Q(end__gt=now()) | Q(end=None),
Q(closed=False) | Q(closed=None),
)
def is_available(self):
if self.closed:
return False
if self.start and self.start > now():
return False
if self.end and self.end < now():
return False
return True
def __str__(self):
return self.section.name
class ProposalKind(models.Model):
"""
e.g. talk vs panel vs tutorial vs poster
Note that if you have different deadlines, reviewers, etc. you'll want
to distinguish the section as well as the kind.
"""
section = models.ForeignKey(Section, related_name="proposal_kinds", verbose_name=_("Section"))
name = models.CharField(_("Name"), max_length=100)
slug = models.SlugField(verbose_name=_("Slug"))
def __str__(self):
return self.name
class ProposalBase(models.Model):
objects = InheritanceManager()
kind = models.ForeignKey(ProposalKind, verbose_name=_("Kind"))
title = models.CharField(max_length=100, verbose_name=_("Title"))
abstract = models.TextField(
_("Abstract"),
help_text=_("This will appear in the conference programme. Up to about "
"500 words. " + constants.TEXT_FIELD_MONOSPACE_NOTE)
)
abstract_html = models.TextField(blank=True)
private_abstract = models.TextField(
_("Private Abstract"),
help_text=_("This will only be shown to organisers and reviewers. You "
"should provide any details about your proposal that you "
"don't want to be public here. " +
constants.TEXT_FIELD_MONOSPACE_NOTE)
)
private_abstract_html = models.TextField(blank=True)
technical_requirements = models.TextField(
_("Special Requirements"),
blank=True,
help_text=_("Speakers will be provided with: Internet access, power, "
"projector, audio. If you require anything in addition, "
"please list your technical requirements here. Such as: a "
"static IP address, A/V equipment or will be demonstrating "
"security-related techniques on the conference network. " +
constants.TEXT_FIELD_MONOSPACE_NOTE)
)
technical_requirements_html = models.TextField(blank=True)
project = models.CharField(
max_length=100,
blank=True,
help_text=_("The name of the project you will be talking about."),
)
project_url = models.URLField(
_("Project URL"),
blank=True,
help_text=_("If your project has a webpage, specify the URL here so "
"the committee can find out more about your proposal.")
)
video_url = models.URLField(
_("Video"),
blank=True,
help_text=_("You may optionally provide us with a link to a video of "
"you speaking at another event, or of a short 'elevator "
"pitch' of your proposed talk.")
)
submitted = models.DateTimeField(
default=now,
editable=False,
verbose_name=_("Submitted")
)
speaker = models.ForeignKey(Speaker, related_name="proposals", verbose_name=_("Speaker"))
# @@@ this validation used to exist as a validators keyword on additional_speakers
# M2M field but that is no longer supported by Django. Should be moved to
# the form level
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",
blank=True, verbose_name=_("Addtional speakers"))
cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled"))
def save(self, *args, **kwargs):
self.abstract_html = parse(self.abstract)
self.private_abstract_html = parse(self.private_abstract)
self.technical_requirements_html = parse(self.technical_requirements)
return super(ProposalBase, self).save(*args, **kwargs)
def can_edit(self):
return True
@property
def section(self):
return self.kind.section
@property
def speaker_email(self):
return self.speaker.email
@property
def number(self):
return str(self.pk).zfill(3)
@property
def status(self):
try:
return self.result.status
except ObjectDoesNotExist:
return _('Undecided')
def speakers(self):
yield self.speaker
speakers = self.additional_speakers.exclude(
additionalspeaker__status=AdditionalSpeaker.SPEAKING_STATUS_DECLINED)
for speaker in speakers:
yield speaker
def notification_email_context(self):
return {
"title": self.title,
"main_speaker": self.speaker,
"speakers": ', '.join([x.name for x in self.speakers()]),
"additional_speakers": self.additional_speakers,
"kind": self.kind.name,
}
def __str__(self):
return self.title
reversion.register(ProposalBase)
class AdditionalSpeaker(models.Model):
SPEAKING_STATUS_PENDING = 1
SPEAKING_STATUS_ACCEPTED = 2
SPEAKING_STATUS_DECLINED = 3
SPEAKING_STATUS = [
(SPEAKING_STATUS_PENDING, _("Pending")),
(SPEAKING_STATUS_ACCEPTED, _("Accepted")),
(SPEAKING_STATUS_DECLINED, _("Declined")),
]
speaker = models.ForeignKey(Speaker, verbose_name=_("Speaker"))
proposalbase = models.ForeignKey(ProposalBase, verbose_name=_("Proposalbase"))
status = models.IntegerField(choices=SPEAKING_STATUS, default=SPEAKING_STATUS_PENDING, verbose_name=_("Status"))
class Meta:
unique_together = ("speaker", "proposalbase")
verbose_name = _("Addtional speaker")
verbose_name_plural = _("Additional speakers")
def __str__(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):
ext = filename.split(".")[-1]
filename = "%s.%s" % (uuid.uuid4(), ext)
return os.path.join("document", filename)
class SupportingDocument(models.Model):
proposal = models.ForeignKey(ProposalBase, related_name="supporting_documents", verbose_name=_("Proposal"))
uploaded_by = models.ForeignKey(User, verbose_name=_("Uploaded by"))
created_at = models.DateTimeField(default=now, verbose_name=_("Created at"))
file = models.FileField(upload_to=uuid_filename, verbose_name=_("File"))
description = models.CharField(max_length=140, verbose_name=_("Description"))
def download_url(self):
return self.file.url

View file

View file

@ -0,0 +1,72 @@
from django import template
from symposion.proposals.models import AdditionalSpeaker
register = template.Library()
class AssociatedProposalsNode(template.Node):
@classmethod
def handle_token(cls, parser, token):
bits = token.split_contents()
if len(bits) == 3 and bits[1] == "as":
return cls(bits[2])
else:
raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
def __init__(self, context_var):
self.context_var = context_var
def render(self, context):
request = context["request"]
if request.user.speaker_profile:
pending = AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED
speaker = request.user.speaker_profile
queryset = AdditionalSpeaker.objects.filter(speaker=speaker, status=pending)
context[self.context_var] = [item.proposalbase for item in queryset]
else:
context[self.context_var] = None
return u""
class PendingProposalsNode(template.Node):
@classmethod
def handle_token(cls, parser, token):
bits = token.split_contents()
if len(bits) == 3 and bits[1] == "as":
return cls(bits[2])
else:
raise template.TemplateSyntaxError("%r takes 'as var'" % bits[0])
def __init__(self, context_var):
self.context_var = context_var
def render(self, context):
request = context["request"]
if request.user.speaker_profile:
pending = AdditionalSpeaker.SPEAKING_STATUS_PENDING
speaker = request.user.speaker_profile
queryset = AdditionalSpeaker.objects.filter(speaker=speaker, status=pending)
context[self.context_var] = [item.proposalbase for item in queryset]
else:
context[self.context_var] = None
return u""
@register.tag
def pending_proposals(parser, token):
"""
{% pending_proposals as pending_proposals %}
"""
return PendingProposalsNode.handle_token(parser, token)
@register.tag
def associated_proposals(parser, token):
"""
{% associated_proposals as associated_proposals %}
"""
return AssociatedProposalsNode.handle_token(parser, token)

32
vendor/symposion/proposals/urls.py vendored Normal file
View file

@ -0,0 +1,32 @@
from django.conf.urls import url
from .views import (
proposal_submit,
proposal_submit_kind,
proposal_detail,
proposal_edit,
proposal_speaker_manage,
proposal_cancel,
proposal_leave,
proposal_pending_join,
proposal_pending_decline,
document_create,
document_delete,
document_download,
)
urlpatterns = [
url(r"^submit/$", proposal_submit, name="proposal_submit"),
url(r"^submit/([\w\-]+)/$", proposal_submit_kind, name="proposal_submit_kind"),
url(r"^(\d+)/$", proposal_detail, name="proposal_detail"),
url(r"^(\d+)/edit/$", proposal_edit, name="proposal_edit"),
url(r"^(\d+)/speakers/$", proposal_speaker_manage, name="proposal_speaker_manage"),
url(r"^(\d+)/cancel/$", proposal_cancel, name="proposal_cancel"),
url(r"^(\d+)/leave/$", proposal_leave, name="proposal_leave"),
url(r"^(\d+)/join/$", proposal_pending_join, name="proposal_pending_join"),
url(r"^(\d+)/decline/$", proposal_pending_decline, name="proposal_pending_decline"),
url(r"^(\d+)/document/create/$", document_create, name="proposal_document_create"),
url(r"^document/(\d+)/delete/$", document_delete, name="proposal_document_delete"),
url(r"^document/(\d+)/([^/]+)$", document_download, name="proposal_document_download"),
]

414
vendor/symposion/proposals/views.py vendored Normal file
View file

@ -0,0 +1,414 @@
import hashlib
import random
import sys
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.urlresolvers import reverse
from django.db.models import Q
from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.shortcuts import render, redirect, get_object_or_404
from django.views import static
from django.contrib import messages
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from symposion.proposals.models import (
ProposalBase, ProposalSection, ProposalKind
)
from symposion.proposals.models import SupportingDocument, AdditionalSpeaker
from symposion.speakers.models import Speaker
from symposion.utils.mail import send_email
from symposion.proposals.forms import (
AddSpeakerForm, SupportingDocumentCreateForm
)
def get_form(name):
dot = name.rindex(".")
mod_name, form_name = name[:dot], name[dot + 1:]
__import__(mod_name)
return getattr(sys.modules[mod_name], form_name)
def proposal_submit(request):
if not request.user.is_authenticated():
messages.info(request, _("To submit a proposal, please "
"<a href='{0}'>log in</a> and create a speaker profile "
"via the dashboard.".format(settings.LOGIN_URL)))
return redirect("home") # @@@ unauth'd speaker info page?
else:
try:
request.user.speaker_profile
except ObjectDoesNotExist:
url = reverse("speaker_create")
messages.info(request, _("To submit a proposal, first "
"<a href='{0}'>create a speaker "
"profile</a>.".format(url)))
return redirect("dashboard")
kinds = []
for proposal_section in ProposalSection.available():
for kind in proposal_section.section.proposal_kinds.all():
kinds.append(kind)
return render(request, "symposion/proposals/proposal_submit.html", {
"kinds": kinds,
})
def proposal_submit_kind(request, kind_slug):
kind = get_object_or_404(ProposalKind, slug=kind_slug)
if not request.user.is_authenticated():
return redirect("home") # @@@ unauth'd speaker info page?
else:
try:
speaker_profile = request.user.speaker_profile
except ObjectDoesNotExist:
return redirect("dashboard")
if not kind.section.proposalsection.is_available():
return redirect("proposal_submit")
form_class = get_form(settings.PROPOSAL_FORMS[kind_slug])
if request.method == "POST":
form = form_class(request.POST)
if form.is_valid():
proposal = form.save(commit=False)
proposal.kind = kind
proposal.speaker = speaker_profile
proposal.save()
form.save_m2m()
messages.success(request, _("Proposal submitted."))
if "add-speakers" in request.POST:
return redirect("proposal_speaker_manage", proposal.pk)
return redirect("dashboard")
else:
form = form_class()
return render(request, "symposion/proposals/proposal_submit_kind.html", {
"kind": kind,
"proposal_form": form,
})
@login_required
def proposal_speaker_manage(request, pk):
queryset = ProposalBase.objects.select_related("speaker")
proposal = get_object_or_404(queryset, pk=pk)
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
if proposal.speaker != request.user.speaker_profile:
raise Http404()
if request.method == "POST":
add_speaker_form = AddSpeakerForm(request.POST, proposal=proposal)
if add_speaker_form.is_valid():
message_ctx = {
"proposal": proposal,
}
def create_speaker_token(email_address):
# create token and look for an existing speaker to prevent
# duplicate tokens and confusing the pending speaker
try:
pending = Speaker.objects.get(
Q(user=None, invite_email=email_address)
)
except Speaker.DoesNotExist:
salt = hashlib.sha1(str(random.random()).encode('UTF-8')).hexdigest()[:5]
saltedemail = (salt + email_address).encode('UTF-8')
token = hashlib.sha1(saltedemail).hexdigest()
pending = Speaker.objects.create(
invite_email=email_address,
invite_token=token,
)
else:
token = pending.invite_token
return pending, token
email_address = add_speaker_form.cleaned_data["email"]
# check if email is on the site now
try:
user = User.objects.get(email=email_address)
except ObjectDoesNotExist:
user = None
except MultipleObjectsReturned:
# FIXME: This is not handled in the previous code, so I'm not
# going to frett on this now, but should be handled as it is
# an occourance that really, really shouldn't occour.
# Previously, code took [0] from the list and continued.
raise NotImplementedError("Non unique email should not occour")
if user:
# should only be one since we enforce unique email
message_ctx["user"] = user
# look for speaker profile
try:
speaker = user.speaker_profile
except ObjectDoesNotExist:
speaker, token = create_speaker_token(email_address)
message_ctx["token"] = token
# fire off email to user to create profile
send_email(
[email_address], "speaker_no_profile",
context=message_ctx
)
else:
# fire off email to user letting them they are loved.
send_email(
[email_address], "speaker_addition",
context=message_ctx
)
else:
speaker, token = create_speaker_token(email_address)
message_ctx["token"] = token
# fire off email letting user know about site and to create
# account and speaker profile
send_email(
[email_address], "speaker_invite",
context=message_ctx
)
invitation, created = AdditionalSpeaker.objects.get_or_create(
proposalbase=proposal.proposalbase_ptr, speaker=speaker)
messages.success(request, "Speaker invited to proposal.")
return redirect("proposal_speaker_manage", proposal.pk)
else:
add_speaker_form = AddSpeakerForm(proposal=proposal)
ctx = {
"proposal": proposal,
"speakers": proposal.speakers(),
"add_speaker_form": add_speaker_form,
}
return render(request, "symposion/proposals/proposal_speaker_manage.html", ctx)
@login_required
def proposal_edit(request, pk):
queryset = ProposalBase.objects.select_related("speaker")
proposal = get_object_or_404(queryset, pk=pk)
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
if request.user != proposal.speaker.user:
raise Http404()
if not proposal.can_edit():
ctx = {
"title": "Proposal editing closed",
"body": "Proposal editing is closed for this session type."
}
return render(request, "symposion/proposals/proposal_error.html", ctx)
form_class = get_form(settings.PROPOSAL_FORMS[proposal.kind.slug])
if request.method == "POST":
form = form_class(request.POST, instance=proposal)
if form.is_valid():
form.save()
if hasattr(proposal, "reviews"):
# Miniconf updates should only email the admins
if proposal.kind.slug == 'miniconf':
users = User.objects.filter(username__in=settings.ADMIN_USERNAMES)
else:
users = User.objects.filter(
Q(review__proposal=proposal) |
Q(proposalmessage__proposal=proposal)
)
users = users.exclude(id=request.user.id).distinct()
for user in users:
ctx = {
"user": request.user,
"proposal": proposal,
}
send_email(
[user.email], "proposal_updated",
context=ctx
)
messages.success(request, "Proposal updated.")
return redirect("proposal_detail", proposal.pk)
else:
form = form_class(instance=proposal)
return render(request, "symposion/proposals/proposal_edit.html", {
"proposal": proposal,
"form": form,
})
@login_required
def proposal_detail(request, pk):
queryset = ProposalBase.objects.select_related("speaker", "speaker__user")
proposal = get_object_or_404(queryset, pk=pk)
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
if request.user not in [p.user for p in proposal.speakers()]:
raise Http404()
if "symposion.reviews" in settings.INSTALLED_APPS:
from symposion.reviews.forms import SpeakerCommentForm
message_form = SpeakerCommentForm()
if request.method == "POST":
message_form = SpeakerCommentForm(request.POST)
if message_form.is_valid():
message = message_form.save(commit=False)
message.user = request.user
message.proposal = proposal
message.save()
ProposalMessage = SpeakerCommentForm.Meta.model
reviewers = User.objects.filter(
id__in=ProposalMessage.objects.filter(
proposal=proposal
).exclude(
user=request.user
).distinct().values_list("user", flat=True)
)
for reviewer in reviewers:
ctx = {
"proposal": proposal,
"message": message,
"reviewer": True,
}
send_email(
[reviewer.email], "proposal_new_message",
context=ctx
)
return redirect(request.path)
else:
message_form = SpeakerCommentForm()
else:
message_form = None
return render(request, "symposion/proposals/proposal_detail.html", {
"proposal": proposal,
"message_form": message_form
})
@login_required
def proposal_cancel(request, pk):
queryset = ProposalBase.objects.select_related("speaker")
proposal = get_object_or_404(queryset, pk=pk)
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
if proposal.speaker.user != request.user:
return HttpResponseForbidden()
if request.method == "POST":
proposal.cancelled = True
proposal.save()
# @@@ fire off email to submitter and other speakers
messages.success(request, "%s has been cancelled" % proposal.title)
return redirect("dashboard")
return render(request, "symposion/proposals/proposal_cancel.html", {
"proposal": proposal,
})
@login_required
def proposal_leave(request, pk):
queryset = ProposalBase.objects.select_related("speaker")
proposal = get_object_or_404(queryset, pk=pk)
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
try:
speaker = proposal.additional_speakers.get(user=request.user)
except ObjectDoesNotExist:
return HttpResponseForbidden()
if request.method == "POST":
proposal.additional_speakers.remove(speaker)
# @@@ fire off email to submitter and other speakers
messages.success(request, "You are no longer speaking on %s" % proposal.title)
return redirect("dashboard")
ctx = {
"proposal": proposal,
}
return render(request, "symposion/proposals/proposal_leave.html", ctx)
@login_required
def proposal_pending_join(request, pk):
proposal = get_object_or_404(ProposalBase, pk=pk)
speaking = get_object_or_404(AdditionalSpeaker, speaker=request.user.speaker_profile,
proposalbase=proposal)
if speaking.status == AdditionalSpeaker.SPEAKING_STATUS_PENDING:
speaking.status = AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED
speaking.save()
messages.success(request, "You have accepted the invitation to join %s" % proposal.title)
return redirect("dashboard")
else:
return redirect("dashboard")
@login_required
def proposal_pending_decline(request, pk):
proposal = get_object_or_404(ProposalBase, pk=pk)
speaking = get_object_or_404(AdditionalSpeaker, speaker=request.user.speaker_profile,
proposalbase=proposal)
if speaking.status == AdditionalSpeaker.SPEAKING_STATUS_PENDING:
speaking.status = AdditionalSpeaker.SPEAKING_STATUS_DECLINED
speaking.save()
messages.success(request, "You have declined to speak on %s" % proposal.title)
return redirect("dashboard")
else:
return redirect("dashboard")
@login_required
def document_create(request, proposal_pk):
queryset = ProposalBase.objects.select_related("speaker")
proposal = get_object_or_404(queryset, pk=proposal_pk)
proposal = ProposalBase.objects.get_subclass(pk=proposal.pk)
if proposal.cancelled:
return HttpResponseForbidden()
if request.method == "POST":
form = SupportingDocumentCreateForm(request.POST, request.FILES)
if form.is_valid():
document = form.save(commit=False)
document.proposal = proposal
document.uploaded_by = request.user
document.save()
return redirect("proposal_detail", proposal.pk)
else:
form = SupportingDocumentCreateForm()
return render(request, "symposion/proposals/document_create.html", {
"proposal": proposal,
"form": form,
})
@login_required
def document_download(request, pk, *args):
document = get_object_or_404(SupportingDocument, pk=pk)
if getattr(settings, "USE_X_ACCEL_REDIRECT", False):
response = HttpResponse()
response["X-Accel-Redirect"] = document.file.url
# delete content-type to allow Gondor to determine the filetype and
# we definitely don't want Django's crappy default :-)
del response["content-type"]
else:
response = static.serve(request, document.file.name, document_root=settings.MEDIA_ROOT)
return response
@login_required
def document_delete(request, pk):
document = get_object_or_404(SupportingDocument, pk=pk, uploaded_by=request.user)
proposal_pk = document.proposal.pk
if request.method == "POST":
document.delete()
return redirect("proposal_detail", proposal_pk)

1
vendor/symposion/reviews/__init__.py vendored Normal file
View file

@ -0,0 +1 @@
default_app_config = "symposion.reviews.apps.ReviewsConfig"

20
vendor/symposion/reviews/admin.py vendored Normal file
View file

@ -0,0 +1,20 @@
from django.contrib import admin
from symposion.reviews.models import NotificationTemplate, ProposalResult, ResultNotification
admin.site.register(
NotificationTemplate,
list_display=[
'label',
'from_address',
'subject'
]
)
admin.site.register(
ProposalResult,
list_display=['proposal', 'status', 'score', 'vote_count', 'accepted']
)
admin.site.register(ResultNotification)

8
vendor/symposion/reviews/apps.py vendored Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class ReviewsConfig(AppConfig):
name = "symposion.reviews"
label = "symposion_reviews"
verbose_name = _("Symposion Reviews")

View file

@ -0,0 +1,11 @@
from symposion.proposals.models import ProposalSection
def reviews(request):
sections = []
for section in ProposalSection.objects.all():
if request.user.has_perm("reviews.can_review_%s" % section.section.slug):
sections.append(section)
return {
"review_sections": sections,
}

49
vendor/symposion/reviews/forms.py vendored Normal file
View file

@ -0,0 +1,49 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from symposion.reviews.models import Review, Comment, ProposalMessage, VOTES
class ReviewForm(forms.ModelForm):
required_css_class = 'label-required'
class Meta:
model = Review
fields = ["vote", "comment"]
def __init__(self, *args, **kwargs):
super(ReviewForm, self).__init__(*args, **kwargs)
self.fields["vote"] = forms.ChoiceField(
widget=forms.RadioSelect(),
choices=VOTES.CHOICES
)
class ReviewCommentForm(forms.ModelForm):
required_css_class = 'label-required'
class Meta:
model = Comment
fields = ["text"]
class SpeakerCommentForm(forms.ModelForm):
required_css_class = 'label-required'
class Meta:
model = ProposalMessage
fields = ["message"]
class BulkPresentationForm(forms.Form):
required_css_class = 'label-required'
talk_ids = forms.CharField(
label=_("Talk ids"),
max_length=500,
help_text=_("Provide a comma seperated list of talk ids to accept.")
)

View file

View file

@ -0,0 +1,12 @@
from django.core.management.base import BaseCommand
from symposion.reviews.models import ReviewAssignment
from symposion.proposals.models import ProposalBase
class Command(BaseCommand):
def handle(self, *args, **options):
for proposal in ProposalBase.objects.filter(cancelled=0):
print("Creating assignments for %s" % proposal.title)
ReviewAssignment.create_assignments(proposal)

View file

@ -0,0 +1,9 @@
from django.core.management.base import BaseCommand
from symposion.reviews.models import ProposalResult
class Command(BaseCommand):
def handle(self, *args, **options):
ProposalResult.full_calculate()

View file

@ -0,0 +1,25 @@
from django.core.management.base import BaseCommand
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from symposion.proposals.models import ProposalSection
class Command(BaseCommand):
def handle(self, *args, **options):
ct, created = ContentType.objects.get_or_create(
model="",
app_label="reviews",
defaults={"name": "reviews"}
)
for ps in ProposalSection.objects.all():
for action in ["review", "manage"]:
perm, created = Permission.objects.get_or_create(
codename="can_%s_%s" % (action, ps.section.slug),
content_type__pk=ct.id,
defaults={"name": "Can %s %s" % (action, ps), "content_type": ct}
)
print(perm)

View file

@ -0,0 +1,16 @@
from django.core.management.base import BaseCommand
from django.db import connections
from symposion.reviews.models import ProposalResult, promote_proposal
class Command(BaseCommand):
def handle(self, *args, **options):
accepted_proposals = ProposalResult.objects.filter(status="accepted")
accepted_proposals = accepted_proposals.order_by("proposal")
for result in accepted_proposals:
promote_proposal(result.proposal)
connections["default"].cursor().execute(
"SELECT setval('schedule_session_id_seq', (SELECT max(id) FROM schedule_session))")

View file

@ -0,0 +1,145 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-17 03:35
import datetime
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('symposion_proposals', '__first__'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('text', models.TextField(verbose_name='Text')),
('text_html', models.TextField(blank=True)),
('public', models.BooleanField(choices=[(True, 'public'), (False, 'private')], default=False, verbose_name='Public')),
('commented_at', models.DateTimeField(default=datetime.datetime.now, verbose_name='Commented at')),
('commenter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Commenter')),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
],
options={
'verbose_name': 'comment',
'verbose_name_plural': 'comments',
},
),
migrations.CreateModel(
name='LatestVote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('vote', models.CharField(choices=[('+2', '+2 \u2014 Good proposal and I will argue for it to be accepted.'), ('+1', '+1 \u2014 OK proposal, but I will not argue for it to be accepted.'), ('-1', '\u22121 \u2014 Weak proposal, but I will not argue strongly against acceptance.'), ('-2', '\u22122 \u2014 Serious issues and I will argue to reject this proposal.'), ('0', 'Abstain - I do not want to review this proposal and I do not want to see it again.')], max_length=2, verbose_name='Vote')),
('submitted_at', models.DateTimeField(default=datetime.datetime.now, editable=False, verbose_name='Submitted at')),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'latest vote',
'verbose_name_plural': 'latest votes',
},
),
migrations.CreateModel(
name='NotificationTemplate',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=100, verbose_name='Label')),
('from_address', models.EmailField(max_length=254, verbose_name='From address')),
('subject', models.CharField(max_length=100, verbose_name='Subject')),
('body', models.TextField(verbose_name='Body')),
],
options={
'verbose_name': 'notification template',
'verbose_name_plural': 'notification templates',
},
),
migrations.CreateModel(
name='ProposalMessage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('message', models.TextField(verbose_name='Message')),
('message_html', models.TextField(blank=True)),
('submitted_at', models.DateTimeField(default=datetime.datetime.now, editable=False, verbose_name='Submitted at')),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='messages', to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'ordering': ['submitted_at'],
'verbose_name': 'proposal message',
'verbose_name_plural': 'proposal messages',
},
),
migrations.CreateModel(
name='ProposalResult',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('score', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=5, verbose_name='Score')),
('comment_count', models.PositiveIntegerField(default=0, verbose_name='Comment count')),
('vote_count', models.PositiveIntegerField(default=0, verbose_name='Vote count')),
('abstain', models.PositiveIntegerField(default=0, verbose_name='Abstain')),
('plus_two', models.PositiveIntegerField(default=0, verbose_name='Plus two')),
('plus_one', models.PositiveIntegerField(default=0, verbose_name='Plus one')),
('minus_one', models.PositiveIntegerField(default=0, verbose_name='Minus one')),
('minus_two', models.PositiveIntegerField(default=0, verbose_name='Minus two')),
('accepted', models.NullBooleanField(choices=[(True, 'accepted'), (False, 'rejected'), (None, 'undecided')], default=None, verbose_name='Accepted')),
('status', models.CharField(choices=[('accepted', 'accepted'), ('rejected', 'rejected'), ('undecided', 'undecided'), ('standby', 'standby')], default='undecided', max_length=20, verbose_name='Status')),
('proposal', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='result', to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
],
options={
'verbose_name': 'proposal_result',
'verbose_name_plural': 'proposal_results',
},
),
migrations.CreateModel(
name='ResultNotification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(default=datetime.datetime.now, verbose_name='Timestamp')),
('to_address', models.EmailField(max_length=254, verbose_name='To address')),
('from_address', models.EmailField(max_length=254, verbose_name='From address')),
('subject', models.CharField(max_length=255, verbose_name='Subject')),
('body', models.TextField(verbose_name='Body')),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='symposion_reviews.NotificationTemplate', verbose_name='Template')),
],
),
migrations.CreateModel(
name='Review',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('vote', models.CharField(blank=True, choices=[('+2', '+2 \u2014 Good proposal and I will argue for it to be accepted.'), ('+1', '+1 \u2014 OK proposal, but I will not argue for it to be accepted.'), ('-1', '\u22121 \u2014 Weak proposal, but I will not argue strongly against acceptance.'), ('-2', '\u22122 \u2014 Serious issues and I will argue to reject this proposal.'), ('0', 'Abstain - I do not want to review this proposal and I do not want to see it again.')], max_length=2, verbose_name='Vote')),
('comment', models.TextField(blank=True, verbose_name='Comment')),
('comment_html', models.TextField(blank=True)),
('submitted_at', models.DateTimeField(default=datetime.datetime.now, editable=False, verbose_name='Submitted at')),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reviews', to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'review',
'verbose_name_plural': 'reviews',
},
),
migrations.CreateModel(
name='ReviewAssignment',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('origin', models.IntegerField(choices=[(0, 'auto-assigned, initial'), (1, 'opted-in'), (2, 'auto-assigned, later')], verbose_name='Origin')),
('assigned_at', models.DateTimeField(default=datetime.datetime.now, verbose_name='Assigned at')),
('opted_out', models.BooleanField(default=False, verbose_name='Opted out')),
('proposal', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_proposals.ProposalBase', verbose_name='Proposal')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
),
migrations.AlterUniqueTogether(
name='latestvote',
unique_together=set([('proposal', 'user')]),
),
]

View file

402
vendor/symposion/reviews/models.py vendored Normal file
View file

@ -0,0 +1,402 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from decimal import Decimal
from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import Q, F
from django.db.models import Case, When, Value
from django.db.models import Count
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from symposion import constants
from symposion.text_parser import parse
from symposion.proposals.models import ProposalBase
from symposion.schedule.models import Presentation
def score_expression():
score = (
(2 * F("plus_two") + F("plus_one")) -
(F("minus_one") + 2 * F("minus_two"))
) / (
F("vote_count") * 1.0
)
return Case(
When(vote_count=0, then=Value("0")), # no divide by zero
default=score,
)
class Votes(object):
ABSTAIN = "0"
PLUS_TWO = "+2"
PLUS_ONE = "+1"
MINUS_ONE = "-1"
MINUS_TWO = "-2"
CHOICES = [
(PLUS_TWO, _("+2 — Good proposal and I will argue for it to be accepted.")),
(PLUS_ONE, _("+1 — OK proposal, but I will not argue for it to be accepted.")),
(MINUS_ONE, _("1 — Weak proposal, but I will not argue strongly against acceptance.")),
(MINUS_TWO, _("2 — Serious issues and I will argue to reject this proposal.")),
(ABSTAIN, _("Abstain - I do not want to review this proposal and I do not want to see it again.")),
]
VOTES = Votes()
class ReviewAssignment(models.Model):
AUTO_ASSIGNED_INITIAL = 0
OPT_IN = 1
AUTO_ASSIGNED_LATER = 2
NUM_REVIEWERS = 3
ORIGIN_CHOICES = [
(AUTO_ASSIGNED_INITIAL, _("auto-assigned, initial")),
(OPT_IN, _("opted-in")),
(AUTO_ASSIGNED_LATER, _("auto-assigned, later")),
]
proposal = models.ForeignKey(ProposalBase, verbose_name=_("Proposal"))
user = models.ForeignKey(User, verbose_name=_("User"))
origin = models.IntegerField(choices=ORIGIN_CHOICES, verbose_name=_("Origin"))
assigned_at = models.DateTimeField(default=datetime.now, verbose_name=_("Assigned at"))
opted_out = models.BooleanField(default=False, verbose_name=_("Opted out"))
@classmethod
def create_assignments(cls, proposal, origin=AUTO_ASSIGNED_INITIAL):
speakers = [proposal.speaker] + list(proposal.additional_speakers.all())
reviewers = User.objects.exclude(
pk__in=[
speaker.user_id
for speaker in speakers
if speaker.user_id is not None
] + [
assignment.user_id
for assignment in ReviewAssignment.objects.filter(
proposal_id=proposal.id)]
).filter(
groups__name="reviewers",
).filter(
Q(reviewassignment__opted_out=False) | Q(reviewassignment=None)
).annotate(
num_assignments=models.Count("reviewassignment")
).order_by(
"num_assignments", "?",
)
num_assigned_reviewers = ReviewAssignment.objects.filter(
proposal_id=proposal.id, opted_out=0).count()
for reviewer in reviewers[:max(0, cls.NUM_REVIEWERS - num_assigned_reviewers)]:
cls._default_manager.create(
proposal=proposal,
user=reviewer,
origin=origin,
)
class ProposalMessage(models.Model):
proposal = models.ForeignKey(ProposalBase, related_name="messages", verbose_name=_("Proposal"))
user = models.ForeignKey(User, verbose_name=_("User"))
message = models.TextField(verbose_name=_("Message"))
message_html = models.TextField(blank=True)
submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))
def save(self, *args, **kwargs):
self.message_html = parse(self.message)
return super(ProposalMessage, self).save(*args, **kwargs)
class Meta:
ordering = ["submitted_at"]
verbose_name = _("proposal message")
verbose_name_plural = _("proposal messages")
class Review(models.Model):
VOTES = VOTES
proposal = models.ForeignKey(ProposalBase, related_name="reviews", verbose_name=_("Proposal"))
user = models.ForeignKey(User, verbose_name=_("User"))
# No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
# like some complicated encoding system.
vote = models.CharField(max_length=2, blank=True, choices=VOTES.CHOICES, verbose_name=_("Vote"))
comment = models.TextField(
blank=True,
verbose_name=_("Comment")
)
comment_html = models.TextField(blank=True)
submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))
def clean(self):
err = {}
if self.vote != VOTES.ABSTAIN and not self.comment.strip():
err["comment"] = ValidationError(_("You must provide a comment"))
if err:
raise ValidationError(err)
def save(self, **kwargs):
self.comment_html = parse(self.comment)
if self.vote:
vote, created = LatestVote.objects.get_or_create(
proposal=self.proposal,
user=self.user,
defaults=dict(
vote=self.vote,
submitted_at=self.submitted_at,
)
)
if not created:
LatestVote.objects.filter(pk=vote.pk).update(vote=self.vote)
self.proposal.result.update_vote(self.vote, previous=vote.vote)
else:
self.proposal.result.update_vote(self.vote)
super(Review, self).save(**kwargs)
def delete(self):
model = self.__class__
user_reviews = model._default_manager.filter(
proposal=self.proposal,
user=self.user,
)
try:
# find the latest review
latest = user_reviews.exclude(pk=self.pk).order_by("-submitted_at")[0]
except IndexError:
# did not find a latest which means this must be the only one.
# treat it as a last, but delete the latest vote.
self.proposal.result.update_vote(self.vote, removal=True)
lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
lv.delete()
else:
# handle that we've found a latest vote
# check if self is the lastest vote
if self == latest:
# self is the latest review; revert the latest vote to the
# previous vote
previous = user_reviews.filter(submitted_at__lt=self.submitted_at)\
.order_by("-submitted_at")[0]
self.proposal.result.update_vote(self.vote, previous=previous.vote, removal=True)
lv = LatestVote.objects.filter(proposal=self.proposal, user=self.user)
lv.update(
vote=previous.vote,
submitted_at=previous.submitted_at,
)
else:
# self is not the latest review so we just need to decrement
# the comment count
self.proposal.result.comment_count = models.F("comment_count") - 1
self.proposal.result.save()
# in all cases we need to delete the review; let's do it!
super(Review, self).delete()
def css_class(self):
return {
self.VOTES.ABSTAIN: "abstain",
self.VOTES.PLUS_TWO: "plus-two",
self.VOTES.PLUS_ONE: "plus-one",
self.VOTES.MINUS_ONE: "minus-one",
self.VOTES.MINUS_TWO: "minus-two",
}[self.vote]
@property
def section(self):
return self.proposal.kind.section.slug
class Meta:
verbose_name = _("review")
verbose_name_plural = _("reviews")
class LatestVote(models.Model):
VOTES = VOTES
proposal = models.ForeignKey(ProposalBase, related_name="votes", verbose_name=_("Proposal"))
user = models.ForeignKey(User, verbose_name=_("User"))
# No way to encode "-0" vs. "+0" into an IntegerField, and I don't feel
# like some complicated encoding system.
vote = models.CharField(max_length=2, choices=VOTES.CHOICES, verbose_name=_("Vote"))
submitted_at = models.DateTimeField(default=datetime.now, editable=False, verbose_name=_("Submitted at"))
class Meta:
unique_together = [("proposal", "user")]
verbose_name = _("latest vote")
verbose_name_plural = _("latest votes")
def css_class(self):
return {
self.VOTES.ABSTAIN: "abstain",
self.VOTES.PLUS_TWO: "plus-two",
self.VOTES.PLUS_ONE: "plus-one",
self.VOTES.MINUS_ONE: "minus-one",
self.VOTES.MINUS_TWO: "minus-two",
}[self.vote]
class ProposalResult(models.Model):
proposal = models.OneToOneField(ProposalBase, related_name="result", verbose_name=_("Proposal"))
score = models.DecimalField(max_digits=5, decimal_places=2, default=Decimal("0.00"), verbose_name=_("Score"))
comment_count = models.PositiveIntegerField(default=0, verbose_name=_("Comment count"))
# vote_count only counts non-abstain votes.
vote_count = models.PositiveIntegerField(default=0, verbose_name=_("Vote count"))
abstain = models.PositiveIntegerField(default=0, verbose_name=_("Abstain"))
plus_two = models.PositiveIntegerField(default=0, verbose_name=_("Plus two"))
plus_one = models.PositiveIntegerField(default=0, verbose_name=_("Plus one"))
minus_one = models.PositiveIntegerField(default=0, verbose_name=_("Minus one"))
minus_two = models.PositiveIntegerField(default=0, verbose_name=_("Minus two"))
accepted = models.NullBooleanField(choices=[
(True, "accepted"),
(False, "rejected"),
(None, "undecided"),
], default=None, verbose_name=_("Accepted"))
status = models.CharField(max_length=20, choices=[
("accepted", _("accepted")),
("rejected", _("rejected")),
("undecided", _("undecided")),
("standby", _("standby")),
], default="undecided", verbose_name=_("Status"))
@classmethod
def full_calculate(cls):
for proposal in ProposalBase.objects.all():
result, created = cls._default_manager.get_or_create(proposal=proposal)
result.update_vote()
def update_vote(self, *a, **k):
proposal = self.proposal
self.comment_count = Review.objects.filter(proposal=proposal).count()
agg = LatestVote.objects.filter(proposal=proposal).values(
"vote"
).annotate(
count=Count("vote")
)
vote_count = {}
# Set the defaults
for option in VOTES.CHOICES:
vote_count[option[0]] = 0
# Set the actual values if present
for d in agg:
vote_count[d["vote"]] = d["count"]
self.abstain = vote_count[VOTES.ABSTAIN]
self.plus_two = vote_count[VOTES.PLUS_TWO]
self.plus_one = vote_count[VOTES.PLUS_ONE]
self.minus_one = vote_count[VOTES.MINUS_ONE]
self.minus_two = vote_count[VOTES.MINUS_TWO]
self.vote_count = sum(i[1] for i in vote_count.items()) - self.abstain
self.save()
model = self.__class__
model._default_manager.filter(pk=self.pk).update(score=score_expression())
class Meta:
verbose_name = _("proposal_result")
verbose_name_plural = _("proposal_results")
class Comment(models.Model):
proposal = models.ForeignKey(ProposalBase, related_name="comments", verbose_name=_("Proposal"))
commenter = models.ForeignKey(User, verbose_name=_("Commenter"))
text = models.TextField(verbose_name=_("Text"))
text_html = models.TextField(blank=True)
# Or perhaps more accurately, can the user see this comment.
public = models.BooleanField(choices=[(True, _("public")), (False, _("private"))], default=False, verbose_name=_("Public"))
commented_at = models.DateTimeField(default=datetime.now, verbose_name=_("Commented at"))
class Meta:
verbose_name = _("comment")
verbose_name_plural = _("comments")
def save(self, *args, **kwargs):
self.text_html = parse(self.text)
return super(Comment, self).save(*args, **kwargs)
class NotificationTemplate(models.Model):
label = models.CharField(max_length=100, verbose_name=_("Label"))
from_address = models.EmailField(verbose_name=_("From address"))
subject = models.CharField(max_length=100, verbose_name=_("Subject"))
body = models.TextField(verbose_name=_("Body"))
class Meta:
verbose_name = _("notification template")
verbose_name_plural = _("notification templates")
class ResultNotification(models.Model):
proposal = models.ForeignKey(ProposalBase, related_name="notifications", verbose_name=_("Proposal"))
template = models.ForeignKey(NotificationTemplate, null=True, blank=True,
on_delete=models.SET_NULL, verbose_name=_("Template"))
timestamp = models.DateTimeField(default=datetime.now, verbose_name=_("Timestamp"))
to_address = models.EmailField(verbose_name=_("To address"))
from_address = models.EmailField(verbose_name=_("From address"))
subject = models.CharField(max_length=255, verbose_name=_("Subject"))
body = models.TextField(verbose_name=_("Body"))
def recipients(self):
for speaker in self.proposal.speakers():
yield speaker.email
def __unicode__(self):
return self.proposal.title + ' ' + self.timestamp.strftime('%Y-%m-%d %H:%M:%S')
@property
def email_args(self):
return (self.subject, self.body, self.from_address, self.recipients())
def promote_proposal(proposal):
if hasattr(proposal, "presentation") and proposal.presentation:
# already promoted
presentation = proposal.presentation
presentation.title = proposal.title
presentation.abstract = proposal.abstract
presentation.speaker = proposal.speaker
presentation.proposal_base = proposal
presentation.save()
presentation.additional_speakers.clear()
else:
presentation = Presentation(
title=proposal.title,
abstract=proposal.abstract,
speaker=proposal.speaker,
section=proposal.section,
proposal_base=proposal,
)
presentation.save()
for speaker in proposal.additional_speakers.all():
presentation.additional_speakers.add(speaker)
presentation.save()
return presentation
def unpromote_proposal(proposal):
if hasattr(proposal, "presentation") and proposal.presentation:
proposal.presentation.delete()
def accepted_proposal(sender, instance=None, **kwargs):
if instance is None:
return
if instance.status == "accepted":
promote_proposal(instance.proposal)
else:
unpromote_proposal(instance.proposal)
post_save.connect(accepted_proposal, sender=ProposalResult)

View file

View file

@ -0,0 +1,13 @@
from django import template
from symposion.reviews.models import ReviewAssignment
register = template.Library()
@register.assignment_tag(takes_context=True)
def review_assignments(context):
request = context["request"]
assignments = ReviewAssignment.objects.filter(user=request.user)
return assignments

44
vendor/symposion/reviews/urls.py vendored Normal file
View file

@ -0,0 +1,44 @@
from django.conf.urls import url
from .views import (
review_section,
review_status,
review_list,
review_admin,
review_bulk_accept,
result_notification,
result_notification_prepare,
result_notification_send,
review_random_proposal,
review_detail,
review_delete,
review_assignments,
review_assignment_opt_out,
review_all_proposals_csv,
)
urlpatterns = [
url(r"^section/(?P<section_slug>[\w\-]+)/all/$", review_section, {"reviewed": "all"}, name="review_section"),
url(r"^section/(?P<section_slug>[\w\-]+)/reviewed/$", review_section, {"reviewed": "reviewed"}, name="user_reviewed"),
url(r"^section/(?P<section_slug>[\w\-]+)/not_reviewed/$", review_section, {"reviewed": "not_reviewed"}, name="user_not_reviewed"),
url(r"^section/(?P<section_slug>[\w\-]+)/random/$", review_random_proposal, name="user_random"),
url(r"^section/(?P<section_slug>[\w\-]+)/assignments/$", review_section, {"assigned": True}, name="review_section_assignments"),
url(r"^section/(?P<section_slug>[\w\-]+)/status/$", review_status, name="review_status"),
url(r"^section/(?P<section_slug>[\w\-]+)/status/(?P<key>\w+)/$", review_status, name="review_status"),
url(r"^section/(?P<section_slug>[\w\-]+)/list_reviewer/(?P<user_pk>\d+)/$", review_list, name="review_list_user"),
url(r"^section/(?P<section_slug>[\w\-]+)/admin/$", review_admin, name="review_admin"),
url(r"^section/(?P<section_slug>[\w\-]+)/admin/accept/$", review_bulk_accept, name="review_bulk_accept"),
url(r"^section/(?P<section_slug>[\w\-]+)/notification/(?P<status>\w+)/$", result_notification, name="result_notification"),
url(r"^section/(?P<section_slug>[\w\-]+)/notification/(?P<status>\w+)/prepare/$", result_notification_prepare, name="result_notification_prepare"),
url(r"^section/(?P<section_slug>[\w\-]+)/notification/(?P<status>\w+)/send/$", result_notification_send, name="result_notification_send"),
url(r"^review/(?P<pk>\d+)/$", review_detail, name="review_detail"),
url(r"^(?P<pk>\d+)/delete/$", review_delete, name="review_delete"),
url(r"^assignments/$", review_assignments, name="review_assignments"),
url(r"^assignment/(?P<pk>\d+)/opt-out/$", review_assignment_opt_out, name="review_assignment_opt_out"),
url(r"^csv$", review_all_proposals_csv, name="review_all_proposals_csv"),
]

19
vendor/symposion/reviews/utils.py vendored Normal file
View file

@ -0,0 +1,19 @@
def has_permission(user, proposal, speaker=False, reviewer=False):
"""
Returns whether or not ther user has permission to review this proposal,
with the specified requirements.
If ``speaker`` is ``True`` then the user can be one of the speakers for the
proposal. If ``reviewer`` is ``True`` the speaker can be a part of the
reviewer group.
"""
if user.is_superuser:
return True
if speaker:
if user == proposal.speaker.user or \
proposal.additional_speakers.filter(user=user).exists():
return True
if reviewer:
if user.groups.filter(name="reviewers").exists():
return True
return False

658
vendor/symposion/reviews/views.py vendored Normal file
View file

@ -0,0 +1,658 @@
import csv
import random
from django.contrib.auth.decorators import login_required
from django.core.mail import send_mass_mail
from django.db.models import Q
from django.http import HttpResponse
from django.http import HttpResponseBadRequest, HttpResponseNotAllowed
from django.shortcuts import render, redirect, get_object_or_404
from django.template import Context, Template
from django.views.decorators.http import require_POST
# @@@ switch to pinax-teams
from symposion.teams.models import Team
from symposion.conf import settings
from symposion.proposals.models import ProposalBase, ProposalSection
from symposion.utils.mail import send_email
from symposion.reviews.forms import ReviewForm, SpeakerCommentForm
from symposion.reviews.forms import BulkPresentationForm
from symposion.reviews.models import (
ReviewAssignment, Review, LatestVote, ProposalResult, NotificationTemplate,
ResultNotification, promote_proposal
)
def access_not_permitted(request):
return render(request, "symposion/reviews/access_not_permitted.html")
def proposals_generator(request, queryset, user_pk=None, check_speaker=True):
for obj in queryset:
# @@@ this sucks; we can do better
if check_speaker:
if request.user in [s.user for s in obj.speakers()]:
continue
try:
obj.result
except ProposalResult.DoesNotExist:
ProposalResult.objects.get_or_create(proposal=obj)
obj.comment_count = obj.result.comment_count
obj.score = obj.result.score
obj.total_votes = obj.result.vote_count
obj.plus_two = obj.result.plus_two
obj.plus_one = obj.result.plus_one
obj.minus_one = obj.result.minus_one
obj.minus_two = obj.result.minus_two
lookup_params = dict(proposal=obj)
if user_pk:
lookup_params["user__pk"] = user_pk
else:
lookup_params["user"] = request.user
try:
obj.user_vote = LatestVote.objects.get(**lookup_params).vote
obj.user_vote_css = LatestVote.objects.get(**lookup_params).css_class()
except LatestVote.DoesNotExist:
obj.user_vote = None
obj.user_vote_css = "no-vote"
yield obj
VOTE_THRESHOLD = settings.SYMPOSION_VOTE_THRESHOLD
POSITIVE = "positive"
NEGATIVE = "negative"
INDIFFERENT = "indifferent"
CONTROVERSIAL = "controversial"
TOO_FEW = "too_few"
REVIEW_STATUS_FILTERS = {
# proposals with at least VOTE_THRESHOLD reviews and at least one +2 and no -2s, sorted by
# the 'score'
POSITIVE: lambda qs: qs.filter(result__vote_count__gte=VOTE_THRESHOLD, result__plus_two__gt=0,
result__minus_two=0).order_by("-result__score"),
# proposals with at least VOTE_THRESHOLD reviews and at least one -2 and no +2s, reverse
# sorted by the 'score'
NEGATIVE: lambda qs: qs.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_two__gt=0,
result__plus_two=0).order_by("result__score"),
# proposals with at least VOTE_THRESHOLD reviews and neither a +2 or a -2, sorted by total
# votes (lowest first)
INDIFFERENT: lambda qs: qs.filter(result__vote_count__gte=VOTE_THRESHOLD, result__minus_two=0,
result__plus_two=0).order_by("result__vote_count"),
# proposals with at least VOTE_THRESHOLD reviews and both a +2 and -2, sorted by total
# votes (highest first)
CONTROVERSIAL: lambda qs: qs.filter(
result__vote_count__gte=VOTE_THRESHOLD, result__plus_two__gt=0,
result__minus_two__gt=0).order_by("-result__vote_count"),
# proposals with fewer than VOTE_THRESHOLD reviews
TOO_FEW: lambda qs: qs.filter(
result__vote_count__lt=VOTE_THRESHOLD).order_by("result__vote_count"),
}
# Returns a list of all proposals, proposals reviewed by the user, or the proposals the user has
# yet to review depending on the link user clicks in dashboard
@login_required
def review_section(request, section_slug, assigned=False, reviewed="all"):
if not request.user.has_perm("reviews.can_review_%s" % section_slug):
return access_not_permitted(request)
section = get_object_or_404(ProposalSection, section__slug=section_slug)
queryset = ProposalBase.objects.filter(kind__section=section.section)
if assigned:
assignments = ReviewAssignment.objects.filter(user=request.user)\
.values_list("proposal__id")
queryset = queryset.filter(id__in=assignments)
# passing reviewed in from reviews.urls and out to review_list for
# appropriate template header rendering
if reviewed == "all":
queryset = queryset.select_related("result").select_subclasses()
reviewed = "all_reviews"
elif reviewed == "reviewed":
queryset = queryset.filter(reviews__user=request.user)
reviewed = "user_reviewed"
else:
queryset = queryset.exclude(reviews__user=request.user).exclude(
speaker__user=request.user)
reviewed = "user_not_reviewed"
# lca2017 #21 -- chairs want to be able to see their own proposals in the list
check_speaker = not request.user.has_perm("reviews.can_manage_%s" % section_slug)
proposals = proposals_generator(request, queryset, check_speaker=check_speaker)
ctx = {
"proposals": proposals,
"section": section,
"reviewed": reviewed,
}
return render(request, "symposion/reviews/review_list.html", ctx)
@login_required
def review_all_proposals_csv(request):
''' Returns a CSV representation of all of the proposals this user has
permisison to review. '''
response = HttpResponse("text/csv")
response['Content-Disposition'] = 'attachment; filename="proposals.csv"'
writer = csv.writer(response, quoting=csv.QUOTE_NONNUMERIC)
queryset = ProposalBase.objects.filter()
# The fields from each proposal object to report in the csv
fields = [
"id", "proposal_type", "speaker_name", "speaker_email", "title",
"submitted", "other_speakers", "speaker_travel",
"speaker_accommodation", "cancelled", "status", "score", "total_votes",
"minus_two", "minus_one", "plus_one", "plus_two",
]
# Fields are the heading
writer.writerow(fields)
for proposal in proposals_generator(request, queryset, check_speaker=False):
proposal.speaker_name = proposal.speaker.name
section_slug = proposal.kind.section.slug
kind_slug = proposal.kind.slug
proposal.proposal_type = kind_slug
proposal.other_speakers = ", ".join(
speaker.name
for speaker in proposal.additional_speakers.all()
)
proposal.speaker_travel = ", ".join(
str(bool(speaker.travel_assistance))
for speaker in proposal.speakers()
)
proposal.speaker_accommodation = ", ".join(
str(bool(speaker.accommodation_assistance))
for speaker in proposal.speakers()
)
if not request.user.has_perm("reviews.can_review_%s" % section_slug):
continue
csv_line = [getattr(proposal, field) for field in fields]
writer.writerow(csv_line)
return response
@login_required
def review_random_proposal(request, section_slug):
# lca2017 #16 view for random proposal
if not request.user.has_perm("reviews.can_review_%s" % section_slug):
return access_not_permitted(request)
section = get_object_or_404(ProposalSection, section__slug=section_slug)
queryset = ProposalBase.objects.filter(kind__section=section.section)
# Remove ones already reviewed
queryset = queryset.exclude(reviews__user=request.user)
# Remove talks the reviewer can't vote on -- their own.
queryset = queryset.exclude(speaker__user=request.user)
queryset = queryset.exclude(additional_speakers__user=request.user)
if len(queryset) == 0:
return redirect("review_section", section_slug=section_slug, reviewed="all")
# Direct reviewers to underreviewed proposals
too_few_set = REVIEW_STATUS_FILTERS[TOO_FEW](queryset)
controversial_set = REVIEW_STATUS_FILTERS[CONTROVERSIAL](queryset)
if len(too_few_set) > 0:
proposals = too_few_set.all()
elif len(controversial_set) > 0 and random.random() < 0.2:
proposals = controversial_set.all()
else:
# Select a proposal with less than the median number of total votes
proposals = proposals_generator(request, queryset, check_speaker=False)
proposals = list(proposals)
proposals.sort(key=lambda proposal: proposal.total_votes)
# The first half is the median or less.
# The +1 means we round _up_.
proposals = proposals[:(len(proposals) + 1) / 2]
# Realistically, there shouldn't be all that many proposals to choose
# from, so this should be cheap.
chosen = random.choice(proposals)
return redirect("review_detail", pk=chosen.pk)
@login_required
def review_list(request, section_slug, user_pk):
# if they're not a reviewer admin and they aren't the person whose
# review list is being asked for, don't let them in
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
if not request.user.pk == user_pk:
return access_not_permitted(request)
queryset = ProposalBase.objects.select_related("speaker__user", "result")
reviewed = LatestVote.objects.filter(user__pk=user_pk).values_list("proposal", flat=True)
queryset = queryset.filter(kind__section__slug=section_slug)
queryset = queryset.filter(pk__in=reviewed)
proposals = queryset.order_by("submitted")
admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)
proposals = proposals_generator(request, proposals, user_pk=user_pk, check_speaker=not admin)
ctx = {
"proposals": proposals,
}
return render(request, "symposion/reviews/review_list.html", ctx)
@login_required
def review_admin(request, section_slug):
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
return access_not_permitted(request)
def reviewers():
already_seen = set()
for team in Team.objects.filter(permissions__codename="can_review_%s" % section_slug):
for membership in team.memberships.filter(Q(state="member") | Q(state="manager")):
user = membership.user
if user.pk in already_seen:
continue
already_seen.add(user.pk)
user.comment_count = Review.objects.filter(
user=user,
proposal__kind__section__slug=section_slug,
).count()
user_votes = LatestVote.objects.filter(
user=user,
proposal__kind__section__slug=section_slug,
)
user.total_votes = user_votes.exclude(
vote=LatestVote.VOTES.ABSTAIN,
).count()
user.plus_two = user_votes.filter(
vote=LatestVote.VOTES.PLUS_TWO,
).count()
user.plus_one = user_votes.filter(
vote=LatestVote.VOTES.PLUS_ONE,
).count()
user.minus_one = user_votes.filter(
vote=LatestVote.VOTES.MINUS_ONE,
).count()
user.minus_two = user_votes.filter(
vote=LatestVote.VOTES.MINUS_TWO,
).count()
user.abstain = user_votes.filter(
vote=LatestVote.VOTES.ABSTAIN,
).count()
if user.total_votes == 0:
user.average = "-"
else:
user.average = (
((user.plus_two * 2) + user.plus_one) -
((user.minus_two * 2) + user.minus_one)
) / (user.total_votes * 1.0)
yield user
reviewers_sorted = list(reviewers())
reviewers_sorted.sort(key=lambda reviewer: 0 - reviewer.total_votes)
ctx = {
"section_slug": section_slug,
"reviewers": reviewers_sorted,
}
return render(request, "symposion/reviews/review_admin.html", ctx)
# FIXME: This view is too complex according to flake8
@login_required
def review_detail(request, pk):
proposals = ProposalBase.objects.select_related("result").select_subclasses()
proposal = get_object_or_404(proposals, pk=pk)
if not request.user.has_perm("reviews.can_review_%s" % proposal.kind.section.slug):
return access_not_permitted(request)
speakers = [s.user for s in proposal.speakers()]
if not request.user.is_superuser and request.user in speakers:
return access_not_permitted(request)
admin = request.user.has_perm("reviews.can_manage_%s" % proposal.kind.section.slug)
try:
latest_vote = LatestVote.objects.get(proposal=proposal, user=request.user)
except LatestVote.DoesNotExist:
latest_vote = None
if request.method == "POST":
if request.user in speakers:
return access_not_permitted(request)
if "vote_submit" in request.POST or "vote_submit_and_random" in request.POST:
review_form = ReviewForm(request.POST)
if review_form.is_valid():
review = review_form.save(commit=False)
review.user = request.user
review.proposal = proposal
review.save()
if "vote_submit_and_random" in request.POST:
next_page = redirect("user_random", proposal.kind.section.slug)
next_page["Location"] += "#invalid_fragment" # Hack.
else:
next_page = redirect(request.path)
return next_page
else:
message_form = SpeakerCommentForm()
elif "message_submit" in request.POST and admin:
message_form = SpeakerCommentForm(request.POST)
if message_form.is_valid():
message = message_form.save(commit=False)
message.user = request.user
message.proposal = proposal
message.save()
for speaker in speakers:
if speaker and speaker.email:
ctx = {
"proposal": proposal,
"message": message,
"reviewer": False,
}
send_email(
[speaker.email], "proposal_new_message",
context=ctx
)
return redirect(request.path)
else:
initial = {}
if latest_vote:
initial["vote"] = latest_vote.vote
if request.user in speakers:
review_form = None
else:
review_form = ReviewForm(initial=initial)
elif "result_submit" in request.POST:
if admin:
result = request.POST["result_submit"]
if result == "accept":
proposal.result.status = "accepted"
proposal.result.save()
elif result == "reject":
proposal.result.status = "rejected"
proposal.result.save()
elif result == "undecide":
proposal.result.status = "undecided"
proposal.result.save()
elif result == "standby":
proposal.result.status = "standby"
proposal.result.save()
return redirect(request.path)
elif "publish_changes" in request.POST:
if admin and proposal.result.status == "accepted":
promote_proposal(proposal)
return redirect(request.path)
else:
initial = {}
if latest_vote:
initial["vote"] = latest_vote.vote
if request.user in speakers:
review_form = None
else:
review_form = ReviewForm(initial=initial)
message_form = SpeakerCommentForm()
proposal.comment_count = proposal.result.comment_count
proposal.total_votes = proposal.result.vote_count
proposal.plus_two = proposal.result.plus_two
proposal.plus_one = proposal.result.plus_one
proposal.minus_one = proposal.result.minus_one
proposal.minus_two = proposal.result.minus_two
reviews = Review.objects.filter(proposal=proposal).order_by("-submitted_at")
messages = proposal.messages.order_by("submitted_at")
return render(request, "symposion/reviews/review_detail.html", {
"proposal": proposal,
"latest_vote": latest_vote,
"reviews": reviews,
"review_messages": messages,
"review_form": review_form,
"message_form": message_form,
"is_manager": admin
})
@login_required
@require_POST
def review_delete(request, pk):
review = get_object_or_404(Review, pk=pk)
section_slug = review.section
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
return access_not_permitted(request)
review = get_object_or_404(Review, pk=pk)
review.delete()
return redirect("review_detail", pk=review.proposal.pk)
@login_required
def review_status(request, section_slug=None, key=None):
if not request.user.has_perm("reviews.can_review_%s" % section_slug):
return access_not_permitted(request)
ctx = {
"section_slug": section_slug,
"vote_threshold": VOTE_THRESHOLD,
}
queryset = ProposalBase.objects.select_related("speaker__user", "result").select_subclasses()
if section_slug:
queryset = queryset.filter(kind__section__slug=section_slug)
proposals = dict((key, filt(queryset)) for key, filt in REVIEW_STATUS_FILTERS.items())
admin = request.user.has_perm("reviews.can_manage_%s" % section_slug)
for status in proposals:
if key and key != status:
continue
proposals[status] = list(proposals_generator(request, proposals[status], check_speaker=not admin))
if key:
ctx.update({
"key": key,
"proposals": proposals[key],
})
else:
ctx["proposals"] = proposals
return render(request, "symposion/reviews/review_stats.html", ctx)
@login_required
def review_assignments(request):
if not request.user.groups.filter(name="reviewers").exists():
return access_not_permitted(request)
assignments = ReviewAssignment.objects.filter(
user=request.user,
opted_out=False
)
return render(request, "symposion/reviews/review_assignment.html", {
"assignments": assignments,
})
@login_required
@require_POST
def review_assignment_opt_out(request, pk):
review_assignment = get_object_or_404(
ReviewAssignment, pk=pk, user=request.user)
if not review_assignment.opted_out:
review_assignment.opted_out = True
review_assignment.save()
ReviewAssignment.create_assignments(
review_assignment.proposal, origin=ReviewAssignment.AUTO_ASSIGNED_LATER)
return redirect("review_assignments")
@login_required
def review_bulk_accept(request, section_slug):
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
return access_not_permitted(request)
if request.method == "POST":
form = BulkPresentationForm(request.POST)
if form.is_valid():
talk_ids = form.cleaned_data["talk_ids"].split(",")
talks = ProposalBase.objects.filter(id__in=talk_ids).select_related("result")
for talk in talks:
talk.result.status = "accepted"
talk.result.save()
return redirect("review_section", section_slug=section_slug)
else:
form = BulkPresentationForm()
return render(request, "symposion/reviews/review_bulk_accept.html", {
"form": form,
})
@login_required
def result_notification(request, section_slug, status):
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
return access_not_permitted(request)
proposals = ProposalBase.objects.filter(kind__section__slug=section_slug, result__status=status).select_related("speaker__user", "result").select_subclasses()
notification_templates = NotificationTemplate.objects.all()
ctx = {
"section_slug": section_slug,
"status": status,
"proposals": proposals,
"notification_templates": notification_templates,
}
return render(request, "symposion/reviews/result_notification.html", ctx)
@login_required
def result_notification_prepare(request, section_slug, status):
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
return access_not_permitted(request)
proposal_pks = []
try:
for pk in request.POST.getlist("_selected_action"):
proposal_pks.append(int(pk))
except ValueError:
return HttpResponseBadRequest()
proposals = ProposalBase.objects.filter(
kind__section__slug=section_slug,
result__status=status,
)
proposals = proposals.filter(pk__in=proposal_pks)
proposals = proposals.select_related("speaker__user", "result")
proposals = proposals.select_subclasses()
notification_template_pk = request.POST.get("notification_template", "")
if notification_template_pk:
notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
else:
notification_template = None
ctx = {
"section_slug": section_slug,
"status": status,
"notification_template": notification_template,
"proposals": proposals,
"proposal_pks": ",".join([str(pk) for pk in proposal_pks]),
}
return render(request, "symposion/reviews/result_notification_prepare.html", ctx)
@login_required
def result_notification_send(request, section_slug, status):
if request.method != "POST":
return HttpResponseNotAllowed(["POST"])
if not request.user.has_perm("reviews.can_manage_%s" % section_slug):
return access_not_permitted(request)
if not all([k in request.POST for k in ["proposal_pks", "from_address", "subject", "body"]]):
return HttpResponseBadRequest()
try:
proposal_pks = [int(pk) for pk in request.POST["proposal_pks"].split(",")]
except ValueError:
return HttpResponseBadRequest()
proposals = ProposalBase.objects.filter(
kind__section__slug=section_slug,
result__status=status,
)
proposals = proposals.filter(pk__in=proposal_pks)
proposals = proposals.select_related("speaker__user", "result")
proposals = proposals.select_subclasses()
notification_template_pk = request.POST.get("notification_template", "")
if notification_template_pk:
notification_template = NotificationTemplate.objects.get(pk=notification_template_pk)
else:
notification_template = None
emails = []
for proposal in proposals:
rn = ResultNotification()
rn.proposal = proposal
rn.template = notification_template
rn.to_address = proposal.speaker_email
rn.from_address = request.POST["from_address"]
proposal_context = proposal.notification_email_context()
rn.subject = Template(request.POST["subject"]).render(
Context({
"proposal": proposal_context
})
)
rn.body = Template(request.POST["body"]).render(
Context({
"proposal": proposal_context
})
)
rn.save()
emails.append(rn.email_args)
send_mass_mail(emails)
return redirect("result_notification", section_slug=section_slug, status=status)

1
vendor/symposion/schedule/__init__.py vendored Normal file
View file

@ -0,0 +1 @@
default_app_config = "symposion.schedule.apps.ScheduleConfig"

57
vendor/symposion/schedule/admin.py vendored Normal file
View file

@ -0,0 +1,57 @@
from django.contrib import admin
from symposion.schedule.models import Schedule, Day, Room, SlotKind, Slot, SlotRoom, Presentation, Session, SessionRole, Track
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_override")
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(
SlotKind,
list_display=["label", "schedule"],
)
admin.site.register(
SlotRoom,
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(SessionRole)
admin.site.register(Presentation, PresentationAdmin)
admin.site.register(Track)

7
vendor/symposion/schedule/apps.py vendored Normal file
View file

@ -0,0 +1,7 @@
from django.apps import AppConfig
class ScheduleConfig(AppConfig):
name = "symposion.schedule"
label = "symposion_schedule"
verbose_name = "Symposion Schedule"

163
vendor/symposion/schedule/forms.py vendored Normal file
View file

@ -0,0 +1,163 @@
import csv
import time
from datetime import datetime
from django import forms
from django.contrib import messages
from django.db import IntegrityError, transaction
from django.db.models import Q
from symposion.schedule.models import (Day, Presentation, Room, SlotKind, Slot,
SlotRoom)
class SlotEditForm(forms.Form):
required_css_class = 'label-required'
def __init__(self, *args, **kwargs):
self.slot = kwargs.pop("slot")
super(SlotEditForm, self).__init__(*args, **kwargs)
# @@@ TODO - Make this configurable
if self.slot.kind.label in ["talk", "tutorial", "keynote"]:
self.fields["presentation"] = self.build_presentation_field()
else:
self.fields["content_override"] = self.build_content_override_field()
def build_presentation_field(self):
kwargs = {}
queryset = Presentation.objects.all()
queryset = queryset.exclude(cancelled=True)
queryset = queryset.order_by("proposal_base__pk")
if self.slot.content:
queryset = queryset.filter(Q(slot=None) | Q(pk=self.slot.content.pk))
kwargs["required"] = False
kwargs["initial"] = self.slot.content
else:
queryset = queryset.filter(slot=None)
kwargs["required"] = True
kwargs["queryset"] = queryset
return forms.ModelChoiceField(**kwargs)
def build_content_override_field(self):
kwargs = {
"label": "Content",
"required": False,
"initial": self.slot.content_override,
}
return forms.CharField(**kwargs)
class ScheduleSectionForm(forms.Form):
required_css_class = 'label-required'
ROOM_KEY = 'room'
DATE_KEY = 'date'
START_KEY = 'time_start'
END_KEY = 'time_end'
KIND = 'kind'
filename = forms.FileField(
label='Select a CSV file to import:',
required=False
)
def __init__(self, *args, **kwargs):
self.schedule = kwargs.pop("schedule")
super(ScheduleSectionForm, self).__init__(*args, **kwargs)
def clean_filename(self):
if 'submit' in self.data:
fname = self.cleaned_data.get('filename')
if not fname or not fname.name.endswith('.csv'):
raise forms.ValidationError(u'Please upload a .csv file')
return fname
def _get_start_end_times(self, data):
"Return start and end time objects"
times = []
for x in [data[self.START_KEY], data[self.END_KEY]]:
try:
time_obj = time.strptime(x, '%I:%M %p')
except:
return messages.ERROR, u'Malformed time found: %s.' % x
time_obj = datetime(100, 1, 1, time_obj.tm_hour, time_obj.tm_min, 00)
times.append(time_obj.time())
return times
def _build_rooms(self, data):
"Get or Create Rooms based on schedule type and set of Tracks"
created_rooms = []
rooms = sorted(set([x[self.ROOM_KEY] for x in data]))
for i, room in enumerate(rooms):
room, created = Room.objects.get_or_create(
schedule=self.schedule, name=room, order=i
)
if created:
created_rooms.append(room)
return created_rooms
def _build_days(self, data):
"Get or Create Days based on schedule type and set of Days"
created_days = []
days = set([x[self.DATE_KEY] for x in data])
for day in days:
try:
date = datetime.strptime(day, "%m/%d/%Y")
except ValueError:
[x.delete() for x in created_days]
return messages.ERROR, u'Malformed data found: %s.' % day
day, created = Day.objects.get_or_create(
schedule=self.schedule, date=date
)
if created:
created_days.append(day)
return created_days
def build_schedule(self):
created_items = []
reader = csv.DictReader(self.cleaned_data.get('filename'))
data = [dict((k.strip(), v.strip()) for k, v in x.items()) for x in reader]
# build rooms
created_items.extend(self._build_rooms(data))
# build_days
created_items.extend(self._build_days(data))
# build Slot -> SlotRoom
for row in data:
room = Room.objects.get(
schedule=self.schedule, name=row[self.ROOM_KEY]
)
date = datetime.strptime(row[self.DATE_KEY], "%m/%d/%Y")
day = Day.objects.get(schedule=self.schedule, date=date)
start, end = self._get_start_end_times(row)
slot_kind, created = SlotKind.objects.get_or_create(
label=row[self.KIND], schedule=self.schedule
)
if created:
created_items.append(slot_kind)
if row[self.KIND] == 'plenary':
slot, created = Slot.objects.get_or_create(
kind=slot_kind, day=day, start=start, end=end
)
if created:
created_items.append(slot)
else:
slot = Slot.objects.create(
kind=slot_kind, day=day, start=start, end=end
)
created_items.append(slot)
try:
with transaction.atomic():
SlotRoom.objects.create(slot=slot, room=room)
except IntegrityError:
# delete all created objects and report error
for x in created_items:
x.delete()
return messages.ERROR, u'An overlap occurred; the import was cancelled.'
return messages.SUCCESS, u'Your schedule has been imported.'
def delete_schedule(self):
self.schedule.day_set.all().delete()
return messages.SUCCESS, u'Your schedule has been deleted.'

30
vendor/symposion/schedule/helpers.py vendored Normal file
View file

@ -0,0 +1,30 @@
"""
This file contains functions that are useful to humans at the shell for
manipulating the database in more natural ways.
"""
from django.db import transaction
from .models import Schedule, Day, Room, Slot, SlotKind, SlotRoom
@transaction.commit_on_success
def create_slot(section_slug, date, kind, start, end, rooms):
schedule = Schedule.objects.get(section__slug=section_slug)
slot = Slot()
slot.day = Day.objects.get(schedule=schedule, date=date)
slot.kind = SlotKind.objects.get(schedule=schedule, label=kind)
slot.start = start
slot.end = end
slot.save(force_insert=True)
if rooms == "all":
rooms_qs = Room.objects.filter(schedule=schedule).order_by("order")
else:
rooms_qs = Room.objects.filter(schedule=schedule, name__in=rooms).order_by("order")
if rooms_qs.count() != len(rooms):
raise Exception("input rooms do not match queried rooms; typo?")
for room in rooms_qs:
slot_room = SlotRoom()
slot_room.slot = slot
slot_room.room = room
slot_room.save(force_insert=True)
print("created {} [start={}; end={}]".format(slot.kind.label, slot.start, slot.end))

View file

@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-17 03:35
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('symposion_proposals', '__first__'),
('symposion_speakers', '__first__'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('symposion_conference', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Day',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateField(verbose_name='Date')),
],
options={
'ordering': ['date'],
'verbose_name': 'date',
'verbose_name_plural': 'dates',
},
),
migrations.CreateModel(
name='Presentation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=100, verbose_name='Title')),
('abstract', models.TextField(verbose_name='Abstract')),
('abstract_html', models.TextField(blank=True)),
('cancelled', models.BooleanField(default=False, verbose_name='Cancelled')),
('additional_speakers', models.ManyToManyField(blank=True, related_name='copresentations', to='symposion_speakers.Speaker', verbose_name='Additional speakers')),
('proposal_base', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='presentation', to='symposion_proposals.ProposalBase', verbose_name='Proposal base')),
('section', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='presentations', to='symposion_conference.Section', verbose_name='Section')),
],
options={
'ordering': ['slot'],
'verbose_name': 'presentation',
'verbose_name_plural': 'presentations',
},
),
migrations.CreateModel(
name='Room',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=65, verbose_name='Name')),
('order', models.PositiveIntegerField(verbose_name='Order')),
],
options={
'verbose_name': 'Room',
'verbose_name_plural': 'Rooms',
},
),
migrations.CreateModel(
name='Schedule',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('published', models.BooleanField(default=True, verbose_name='Published')),
('hidden', models.BooleanField(default=False, verbose_name='Hide schedule from overall conference view')),
('section', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='symposion_conference.Section', verbose_name='Section')),
],
options={
'ordering': ['section'],
'verbose_name': 'Schedule',
'verbose_name_plural': 'Schedules',
},
),
migrations.CreateModel(
name='Session',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('day', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sessions', to='symposion_schedule.Day', verbose_name='Day')),
],
options={
'verbose_name': 'Session',
'verbose_name_plural': 'Sessions',
},
),
migrations.CreateModel(
name='SessionRole',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('role', models.IntegerField(choices=[(1, 'Session Chair'), (2, 'Session Runner')], verbose_name='Role')),
('status', models.NullBooleanField(verbose_name='Status')),
('submitted', models.DateTimeField(default=datetime.datetime.now)),
('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Session', verbose_name='Session')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'verbose_name': 'Session role',
'verbose_name_plural': 'Session roles',
},
),
migrations.CreateModel(
name='Slot',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(editable=False, max_length=100)),
('start', models.TimeField(verbose_name='Start')),
('end', models.TimeField(verbose_name='End')),
('content_override', models.TextField(blank=True, verbose_name='Content override')),
('content_override_html', models.TextField(blank=True)),
('day', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Day', verbose_name='Day')),
],
options={
'ordering': ['day', 'start', 'end'],
'verbose_name': 'slot',
'verbose_name_plural': 'slots',
},
),
migrations.CreateModel(
name='SlotKind',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=50, verbose_name='Label')),
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Schedule', verbose_name='schedule')),
],
options={
'verbose_name': 'Slot kind',
'verbose_name_plural': 'Slot kinds',
},
),
migrations.CreateModel(
name='SlotRoom',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Room', verbose_name='Room')),
('slot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Slot', verbose_name='Slot')),
],
options={
'ordering': ['slot', 'room__order'],
'verbose_name': 'Slot room',
'verbose_name_plural': 'Slot rooms',
},
),
migrations.AddField(
model_name='slot',
name='kind',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.SlotKind', verbose_name='Kind'),
),
migrations.AddField(
model_name='session',
name='slots',
field=models.ManyToManyField(related_name='sessions', to='symposion_schedule.Slot', verbose_name='Slots'),
),
migrations.AddField(
model_name='room',
name='schedule',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Schedule', verbose_name='Schedule'),
),
migrations.AddField(
model_name='presentation',
name='slot',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='content_ptr', to='symposion_schedule.Slot', verbose_name='Slot'),
),
migrations.AddField(
model_name='presentation',
name='speaker',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='presentations', to='symposion_speakers.Speaker', verbose_name='Speaker'),
),
migrations.AddField(
model_name='day',
name='schedule',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Schedule', verbose_name='Schedule'),
),
migrations.AlterUniqueTogether(
name='slotroom',
unique_together=set([('slot', 'room')]),
),
migrations.AlterUniqueTogether(
name='sessionrole',
unique_together=set([('session', 'user', 'role')]),
),
migrations.AlterUniqueTogether(
name='day',
unique_together=set([('schedule', 'date')]),
),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-18 00:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('symposion_schedule', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='presentation',
name='unpublish',
field=models.BooleanField(default=False, verbose_name='Do not publish'),
),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-11-13 04:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('symposion_schedule', '0002_presentation_unpublish'),
]
operations = [
migrations.AlterField(
model_name='slot',
name='name',
field=models.CharField(editable=False, max_length=150),
),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-12-09 20:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('symposion_schedule', '0002_presentation_unpublish'),
]
operations = [
migrations.AddField(
model_name='slot',
name='exclusive',
field=models.BooleanField(default=False, help_text='Set to true if this is the only event during this timeslot'),
),
]

View file

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-12-10 06:05
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('symposion_schedule', '0003_slot_exclusive'),
('symposion_schedule', '0003_auto_20161113_1530'),
]
operations = [
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-12-10 06:36
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('symposion_schedule', '0004_merge'),
]
operations = [
migrations.AlterField(
model_name='slot',
name='name',
field=models.CharField(editable=False, max_length=512),
),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-12-24 00:10
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('symposion_schedule', '0005_auto_20161210_1736'),
]
operations = [
migrations.AddField(
model_name='room',
name='track',
field=models.CharField(blank=True, default=None, max_length=80, null=True, verbose_name='Track'),
),
]

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-12-24 06:09
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('symposion_schedule', '0006_room_track'),
]
operations = [
migrations.CreateModel(
name='Track',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=80, verbose_name='Track')),
('day', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Day')),
],
options={
'verbose_name': 'Track',
'verbose_name_plural': 'Tracks',
},
),
migrations.RemoveField(
model_name='room',
name='track',
),
migrations.AddField(
model_name='track',
name='room',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_schedule.Room'),
),
migrations.AlterUniqueTogether(
name='track',
unique_together=set([('room', 'day')]),
),
]

View file

307
vendor/symposion/schedule/models.py vendored Normal file
View file

@ -0,0 +1,307 @@
import datetime
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import ugettext_lazy as _
from symposion.text_parser import parse
from symposion.proposals.models import ProposalBase
from symposion.conference.models import Section
from symposion.speakers.models import Speaker
class Schedule(models.Model):
section = models.OneToOneField(Section, verbose_name=_("Section"))
published = models.BooleanField(default=True, verbose_name=_("Published"))
hidden = models.BooleanField(_("Hide schedule from overall conference view"), default=False)
def __str__(self):
return "%s Schedule" % self.section
class Meta:
ordering = ["section"]
verbose_name = _('Schedule')
verbose_name_plural = _('Schedules')
class Day(models.Model):
schedule = models.ForeignKey(Schedule, verbose_name=_("Schedule"))
date = models.DateField(verbose_name=_("Date"))
def __str__(self):
return "%s" % self.date
class Meta:
unique_together = [("schedule", "date")]
ordering = ["date"]
verbose_name = _("date")
verbose_name_plural = _("dates")
class Room(models.Model):
schedule = models.ForeignKey(Schedule, verbose_name=_("Schedule"))
name = models.CharField(max_length=65, verbose_name=_("Name"))
order = models.PositiveIntegerField(verbose_name=_("Order"))
def __str__(self):
return self.name
class Meta:
verbose_name = _("Room")
verbose_name_plural = _("Rooms")
class Track(models.Model):
name = models.CharField(max_length=80, verbose_name=_("Track"))
room = models.ForeignKey(Room)
day = models.ForeignKey(Day)
def __str__(self):
return self.name
class Meta:
unique_together = [('room', 'day')]
verbose_name = _("Track")
verbose_name_plural = _("Tracks")
class SlotKind(models.Model):
"""
A slot kind represents what kind a slot is. For example, a slot can be a
break, lunch, or X-minute talk.
"""
schedule = models.ForeignKey(Schedule, verbose_name=_("schedule"))
label = models.CharField(max_length=50, verbose_name=_("Label"))
def __str__(self):
return self.label
class Meta:
verbose_name = _("Slot kind")
verbose_name_plural = _("Slot kinds")
class Slot(models.Model):
name = models.CharField(max_length=512, editable=False)
day = models.ForeignKey(Day, verbose_name=_("Day"))
kind = models.ForeignKey(SlotKind, verbose_name=_("Kind"))
start = models.TimeField(verbose_name=_("Start"))
end = models.TimeField(verbose_name=_("End"))
exclusive = models.BooleanField(
default=False,
help_text=_("Set to true if this is the only event during this "
"timeslot"),
)
content_override = models.TextField(blank=True, verbose_name=_("Content override"))
content_override_html = models.TextField(blank=True)
def assign(self, content):
"""
Assign the given content to this slot and if a previous slot content
was given we need to unlink it to avoid integrity errors.
"""
self.unassign()
content.slot = self
content.save()
def unassign(self):
"""
Unassign the associated content with this slot.
"""
content = self.content
if content and content.slot_id:
content.slot = None
content.save()
@property
def content(self):
"""
Return the content this slot represents.
@@@ hard-coded for presentation for now
"""
try:
return self.content_ptr
except ObjectDoesNotExist:
return None
@property
def start_datetime(self):
return datetime.datetime(
self.day.date.year,
self.day.date.month,
self.day.date.day,
self.start.hour,
self.start.minute)
@property
def end_datetime(self):
return datetime.datetime(
self.day.date.year,
self.day.date.month,
self.day.date.day,
self.end.hour,
self.end.minute)
@property
def length_in_minutes(self):
return int(
(self.end_datetime - self.start_datetime).total_seconds() / 60)
@property
def rooms(self):
return Room.objects.filter(pk__in=self.slotroom_set.values("room"))
def save(self, *args, **kwargs):
roomlist = ' '.join(map(lambda r: r.__unicode__(), self.rooms))
self.name = "%s %s (%s - %s) %s" % (self.day, self.kind, self.start, self.end, roomlist)
self.content_override_html = parse(self.content_override)
super(Slot, self).save(*args, **kwargs)
def __str__(self):
return self.name
class Meta:
ordering = ["day", "start", "end"]
verbose_name = _("slot")
verbose_name_plural = _("slots")
class SlotRoom(models.Model):
"""
Links a slot with a room.
"""
slot = models.ForeignKey(Slot, verbose_name=_("Slot"))
room = models.ForeignKey(Room, verbose_name=_("Room"))
def __str__(self):
return "%s %s" % (self.room, self.slot)
class Meta:
unique_together = [("slot", "room")]
ordering = ["slot", "room__order"]
verbose_name = _("Slot room")
verbose_name_plural = _("Slot rooms")
class Presentation(models.Model):
slot = models.OneToOneField(Slot, null=True, blank=True, related_name="content_ptr", verbose_name=_("Slot"))
title = models.CharField(max_length=100, verbose_name=_("Title"))
abstract = models.TextField(verbose_name=_("Abstract"))
abstract_html = models.TextField(blank=True)
speaker = models.ForeignKey(Speaker, related_name="presentations", verbose_name=_("Speaker"))
additional_speakers = models.ManyToManyField(Speaker, related_name="copresentations",
blank=True, verbose_name=_("Additional speakers"))
unpublish = models.BooleanField(
default=False,
verbose_name=_("Do not publish"),
)
cancelled = models.BooleanField(default=False, verbose_name=_("Cancelled"))
proposal_base = models.OneToOneField(ProposalBase, related_name="presentation", verbose_name=_("Proposal base"))
section = models.ForeignKey(Section, related_name="presentations", verbose_name=_("Section"))
def save(self, *args, **kwargs):
self.abstract_html = parse(self.abstract)
return super(Presentation, self).save(*args, **kwargs)
@property
def number(self):
return self.proposal.number
@property
def proposal(self):
if self.proposal_base_id is None:
return None
return ProposalBase.objects.get_subclass(pk=self.proposal_base_id)
def speakers(self):
yield self.speaker
for speaker in self.additional_speakers.all():
if speaker.user:
yield speaker
def __str__(self):
return "#%s %s (%s)" % (self.number, self.title, self.speaker)
class Meta:
ordering = ["slot"]
verbose_name = _("presentation")
verbose_name_plural = _("presentations")
class Session(models.Model):
day = models.ForeignKey(Day, related_name="sessions", verbose_name=_("Day"))
slots = models.ManyToManyField(Slot, related_name="sessions", verbose_name=_("Slots"))
def sorted_slots(self):
return self.slots.order_by("start")
def start(self):
slots = self.sorted_slots()
if slots:
return list(slots)[0].start
else:
return None
def end(self):
slots = self.sorted_slots()
if slots:
return list(slots)[-1].end
else:
return None
def chair(self):
for role in self.sessionrole_set.all():
if role.role == SessionRole.SESSION_ROLE_CHAIR:
return role
return None
def __str__(self):
start = self.start()
end = self.end()
if start and end:
return "%s: %s - %s" % (
self.day.date.strftime("%a"),
start.strftime("%X"),
end.strftime("%X")
)
return ""
class Meta:
verbose_name = _("Session")
verbose_name_plural = _("Sessions")
class SessionRole(models.Model):
SESSION_ROLE_CHAIR = 1
SESSION_ROLE_RUNNER = 2
SESSION_ROLE_TYPES = [
(SESSION_ROLE_CHAIR, _("Session Chair")),
(SESSION_ROLE_RUNNER, _("Session Runner")),
]
session = models.ForeignKey(Session, verbose_name=_("Session"))
user = models.ForeignKey(User, verbose_name=_("User"))
role = models.IntegerField(choices=SESSION_ROLE_TYPES, verbose_name=_("Role"))
status = models.NullBooleanField(verbose_name=_("Status"))
submitted = models.DateTimeField(default=datetime.datetime.now)
class Meta:
unique_together = [("session", "user", "role")]
verbose_name = _("Session role")
verbose_name_plural = _("Session roles")
def __str__(self):
return "%s %s: %s" % (self.user, self.session,
self.SESSION_ROLE_TYPES[self.role - 1][1])

View file

View file

@ -0,0 +1,13 @@
"date","time_start","time_end","kind"," room "
"12/12/2013","10:00 AM","11:00 AM","plenary","Room2"
"12/12/2013","10:00 AM","11:00 AM","plenary","Room1"
"12/12/2013","11:00 AM","12:00 PM","talk","Room1"
"12/12/2013","11:00 AM","12:00 PM","talk","Room2"
"12/12/2013","12:00 PM","12:45 PM","plenary","Room1"
"12/12/2013","12:00 PM","12:45 PM","plenary","Room2"
"12/13/2013","10:00 AM","11:00 AM","plenary","Room2"
"12/13/2013","10:00 AM","11:00 AM","plenary","Room1"
"12/13/2013","11:00 AM","12:00 PM","talk","Room1"
"12/13/2013","11:00 AM","12:00 PM","talk","Room2"
"12/13/2013","12:00 PM","12:45 PM","plenary","Room1"
"12/13/2013","12:00 PM","12:45 PM","plenary","Room2"
1 date time_start time_end kind room
2 12/12/2013 10:00 AM 11:00 AM plenary Room2
3 12/12/2013 10:00 AM 11:00 AM plenary Room1
4 12/12/2013 11:00 AM 12:00 PM talk Room1
5 12/12/2013 11:00 AM 12:00 PM talk Room2
6 12/12/2013 12:00 PM 12:45 PM plenary Room1
7 12/12/2013 12:00 PM 12:45 PM plenary Room2
8 12/13/2013 10:00 AM 11:00 AM plenary Room2
9 12/13/2013 10:00 AM 11:00 AM plenary Room1
10 12/13/2013 11:00 AM 12:00 PM talk Room1
11 12/13/2013 11:00 AM 12:00 PM talk Room2
12 12/13/2013 12:00 PM 12:45 PM plenary Room1
13 12/13/2013 12:00 PM 12:45 PM plenary Room2

View file

@ -0,0 +1,14 @@
"date","time_start","time_end","kind"," room "
"12/12/2013","10:00 AM","11:00 AM","plenary","Room2"
"12/12/2013","10:00 AM","11:00 AM","plenary","Room1"
"12/12/2013","11:00 AM","12:00 PM","talk","Room1"
"12/12/2013","11:00 AM","12:00 PM","talk","Room2"
"12/12/2013","12:00 PM","12:45 PM","plenary","Room1"
"12/12/2013","12:00 PM","12:45 PM","plenary","Room2"
"12/13/2013","10:00 AM","11:00 AM","plenary","Room2"
"12/13/2013","10:00 AM","11:00 AM","plenary","Room1"
"12/13/2013","11:00 AM","12:00 PM","talk","Room1"
"12/13/2013","11:00 AM","12:00 PM","talk","Room2"
"12/13/2013","12:00 PM","12:45 PM","plenary","Room1"
"12/13/2013","12:00 PM","12:45 PM","plenary","Room2"
"12/13/2013","12:00 PM","12:45 PM","plenary","Room2"
1 date time_start time_end kind room
2 12/12/2013 10:00 AM 11:00 AM plenary Room2
3 12/12/2013 10:00 AM 11:00 AM plenary Room1
4 12/12/2013 11:00 AM 12:00 PM talk Room1
5 12/12/2013 11:00 AM 12:00 PM talk Room2
6 12/12/2013 12:00 PM 12:45 PM plenary Room1
7 12/12/2013 12:00 PM 12:45 PM plenary Room2
8 12/13/2013 10:00 AM 11:00 AM plenary Room2
9 12/13/2013 10:00 AM 11:00 AM plenary Room1
10 12/13/2013 11:00 AM 12:00 PM talk Room1
11 12/13/2013 11:00 AM 12:00 PM talk Room2
12 12/13/2013 12:00 PM 12:45 PM plenary Room1
13 12/13/2013 12:00 PM 12:45 PM plenary Room2
14 12/13/2013 12:00 PM 12:45 PM plenary Room2

View file

@ -0,0 +1,65 @@
import datetime
import random
import factory
from factory import fuzzy
from symposion.schedule.models import Schedule, Day, Slot, SlotKind
from symposion.conference.models import Section, Conference
class ConferenceFactory(factory.DjangoModelFactory):
title = fuzzy.FuzzyText()
start_date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1))
end_date = fuzzy.FuzzyDate(
datetime.date(2014, 1, 1) + datetime.timedelta(days=random.randint(1, 10))
)
# timezone = TimeZoneField("UTC")
class Meta:
model = Conference
class SectionFactory(factory.DjangoModelFactory):
conference = factory.SubFactory(ConferenceFactory)
name = fuzzy.FuzzyText()
slug = fuzzy.FuzzyText()
class Meta:
model = Section
class ScheduleFactory(factory.DjangoModelFactory):
section = factory.SubFactory(SectionFactory)
published = True
hidden = False
class Meta:
model = Schedule
class SlotKindFactory(factory.DjangoModelFactory):
schedule = factory.SubFactory(ScheduleFactory)
label = fuzzy.FuzzyText()
class Meta:
model = SlotKind
class DayFactory(factory.DjangoModelFactory):
schedule = factory.SubFactory(ScheduleFactory)
date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1))
class Meta:
model = Day
class SlotFactory(factory.DjangoModelFactory):
day = factory.SubFactory(DayFactory)
kind = factory.SubFactory(SlotKindFactory)
start = datetime.time(random.randint(0, 23), random.randint(0, 59))
end = datetime.time(random.randint(0, 23), random.randint(0, 59))
class Meta:
model = Slot

65
vendor/symposion/schedule/tests/runtests.py vendored Executable file
View file

@ -0,0 +1,65 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# see runtests.py in https://github.com/pydanny/cookiecutter-djangopackage
import sys
try:
from django.conf import settings
settings.configure(
DEBUG=True,
USE_TZ=True,
DATABASES={
"default": {
"ENGINE": "django.db.backends.sqlite3",
}
},
ROOT_URLCONF="symposion.schedule.urls",
INSTALLED_APPS=[
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sites",
"reversion",
"symposion",
"symposion.conference",
"symposion.speakers",
"symposion.schedule",
"symposion.proposals",
],
SITE_ID=1,
NOSE_ARGS=['-s'],
)
try:
import django
setup = django.setup
except AttributeError:
pass
else:
setup()
from django_nose import NoseTestSuiteRunner
except ImportError:
raise ImportError("To fix this error, run: pip install -r requirements-test.txt")
def run_tests(*test_args):
if not test_args:
test_args = ['tests']
# Run tests
test_runner = NoseTestSuiteRunner(verbosity=1)
failures = test_runner.run_tests(test_args)
if failures:
sys.exit(failures)
if __name__ == '__main__':
run_tests("symposion.schedule.tests.test_views")

View file

@ -0,0 +1,156 @@
import os
from datetime import datetime, timedelta
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase
from symposion.conference.models import Conference, Section
from ..forms import ScheduleSectionForm
from ..models import Day, Room, Schedule, Slot, SlotKind
DATA_DIR = os.path.join(os.path.dirname(__file__), 'data')
class ScheduleSectionFormTests(TestCase):
def setUp(self):
self.conference = Conference.objects.create(title='test')
self.section = Section.objects.create(
conference=self.conference,
name='test')
self.schedule = Schedule.objects.create(section=self.section)
self.today = datetime.now()
self.tomorrow = self.today + timedelta(days=1)
def test_clean_filename(self):
"""Ensure a file is provided if the submit action was utilized"""
data = {'submit': 'Submit'}
form = ScheduleSectionForm(data=data, schedule=self.schedule)
self.assertIn('filename', form.errors)
def test_clean_filename_not_required(self):
"""Ensure file is not required if the delete action was utilize"""
data = {'delete': 'Delete'}
form = ScheduleSectionForm(data=data, schedule=self.schedule)
self.assertTrue(form.is_valid())
def test_delete(self):
"""Delete schedule (Days) for supplied section"""
Day.objects.create(schedule=self.schedule, date=self.today)
Day.objects.create(schedule=self.schedule, date=self.tomorrow)
other_section = Section.objects.create(conference=self.conference, name='other')
other_schedule = Schedule.objects.create(section=other_section)
other_day = Day.objects.create(schedule=other_schedule, date=self.tomorrow)
self.assertEqual(3, Day.objects.all().count())
data = {'delete': 'Delete'}
form = ScheduleSectionForm(data=data, schedule=self.schedule)
form.delete_schedule()
days = Day.objects.all()
self.assertEqual(1, days.count())
self.assertIn(other_day, days)
def test_build_days(self):
"""Test private method to build days based off ingested CSV"""
form = ScheduleSectionForm(schedule=self.schedule)
data = (
{'date': datetime.strftime(self.today, "%m/%d/%Y")},
{'date': datetime.strftime(self.today, "%m/%d/%Y")},
{'date': datetime.strftime(self.tomorrow, "%m/%d/%Y")},
)
self.assertEqual(0, Day.objects.all().count())
form._build_days(data)
self.assertEqual(2, Day.objects.all().count())
def test_build_days_malformed(self):
"""Test failure for malformed date in CSV"""
form = ScheduleSectionForm(schedule=self.schedule)
data = (
{'date': datetime.strftime(self.today, "%m/%d/%Y")},
{'date': '12-12-12'}
)
self.assertEqual(0, Day.objects.all().count())
msg_type, msg = form._build_days(data)
self.assertEqual(0, Day.objects.all().count())
self.assertEqual(40, msg_type)
self.assertIn('12-12-12', msg)
def test_build_rooms(self):
"""Test private method to build rooms based off ingested CSV"""
form = ScheduleSectionForm(schedule=self.schedule)
data = (
{'room': 'foo'},
{'room': 'bar'},
{'room': 'foo'},
)
self.assertEqual(0, Room.objects.all().count())
form._build_rooms(data)
self.assertEqual(2, Room.objects.all().count())
def test_get_start_end_times(self):
"""
Test private method to convert start and end times based off
ingested CSV
"""
form = ScheduleSectionForm(schedule=self.schedule)
start = '12:00 PM'
end = '01:00 PM'
data = {'time_start': start, 'time_end': end}
start_time, end_time = form._get_start_end_times(data)
self.assertEqual(start, start_time.strftime('%I:%M %p'))
self.assertEqual(end, end_time.strftime('%I:%M %p'))
def test_get_start_end_times_malformed(self):
"""
Test private method for malformed time based off ingested CSV
"""
form = ScheduleSectionForm(schedule=self.schedule)
start = '12:00'
end = '01:00'
data = {'time_start': start, 'time_end': end}
msg_type, msg = form._get_start_end_times(data)
self.assertEqual(40, msg_type)
self.assertIn('Malformed', msg)
def test_build_schedule(self):
"""
Test successful schedule build based off ingested CSV
"""
self.assertEqual(0, Day.objects.all().count())
self.assertEqual(0, Room.objects.all().count())
self.assertEqual(0, Slot.objects.all().count())
self.assertEqual(0, SlotKind.objects.all().count())
schedule_csv = open(os.path.join(DATA_DIR, 'schedule.csv'), 'rb')
file_data = {'filename': SimpleUploadedFile(schedule_csv.name, schedule_csv.read())}
data = {'submit': 'Submit'}
form = ScheduleSectionForm(data, file_data, schedule=self.schedule)
form.is_valid()
msg_type, msg = form.build_schedule()
self.assertEqual(25, msg_type)
self.assertIn('imported', msg)
self.assertEqual(2, Day.objects.all().count())
self.assertEqual(2, Room.objects.all().count())
self.assertEqual(8, Slot.objects.all().count())
self.assertEqual(2, SlotKind.objects.all().count())
def test_build_schedule_overlap(self):
"""
Test rolledback schedule build based off ingested CSV with Slot overlap
"""
self.assertEqual(0, Day.objects.all().count())
self.assertEqual(0, Room.objects.all().count())
self.assertEqual(0, Slot.objects.all().count())
self.assertEqual(0, SlotKind.objects.all().count())
schedule_csv = open(os.path.join(DATA_DIR, 'schedule_overlap.csv'), 'rb')
file_data = {'filename': SimpleUploadedFile(schedule_csv.name, schedule_csv.read())}
data = {'submit': 'Submit'}
form = ScheduleSectionForm(data, file_data, schedule=self.schedule)
form.is_valid()
msg_type, msg = form.build_schedule()
self.assertEqual(40, msg_type)
self.assertIn('overlap', msg)
self.assertEqual(0, Day.objects.all().count())
self.assertEqual(0, Room.objects.all().count())
self.assertEqual(0, Slot.objects.all().count())
self.assertEqual(0, SlotKind.objects.all().count())

View file

@ -0,0 +1,30 @@
import json
from django.test.client import Client
from django.test import TestCase
from . import factories
class ScheduleViewTests(TestCase):
def test_empty_json(self):
c = Client()
r = c.get('/conference.json')
assert r.status_code == 200
conference = json.loads(r.content)
assert 'schedule' in conference
assert len(conference['schedule']) == 0
def test_populated_empty_presentations(self):
factories.SlotFactory.create_batch(size=5)
c = Client()
r = c.get('/conference.json')
assert r.status_code == 200
conference = json.loads(r.content)
assert 'schedule' in conference
assert len(conference['schedule']) == 5

View file

@ -0,0 +1,58 @@
from datetime import date
from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.test import TestCase
from symposion.conference.models import Section, current_conference, Conference
from symposion.schedule.models import Day, Schedule, Session
class TestScheduleViews(TestCase):
username = "user@example.com"
first_name = "Sam"
last_name = "McGillicuddy"
def setUp(self):
self.user = User.objects.create_user(self.username,
password="pass",
email=self.username)
self.user.first_name = self.first_name
self.user.last_name = self.last_name
self.user.save()
def test_session_list(self):
# Really minimal test for session list
rsp = self.client.get(reverse("schedule_session_list"))
self.assertEqual(200, rsp.status_code)
def test_session_staff_email(self):
# login and staff required
self.user.is_staff = True
self.user.save()
assert self.client.login(username=self.username, password="pass")
url = reverse("schedule_session_staff_email")
rsp = self.client.get(url)
self.assertEqual(200, rsp.status_code)
def test_session_detail(self):
# really minimal test
Conference.objects.get_or_create(id=settings.CONFERENCE_ID)
section = Section.objects.create(
conference=current_conference(),
)
schedule = Schedule.objects.create(
section=section,
)
day = Day.objects.create(
schedule=schedule,
date=date.today(),
)
session = Session.objects.create(
day=day,
)
url = reverse("schedule_session_detail", args=(session.pk,))
rsp = self.client.get(url)
self.assertEqual(200, rsp.status_code)

51
vendor/symposion/schedule/timetable.py vendored Normal file
View file

@ -0,0 +1,51 @@
import itertools
from django.db.models import Count, Min
from symposion.schedule.models import Room, Slot, SlotRoom
class TimeTable(object):
def __init__(self, day):
self.day = day
def slots_qs(self):
qs = Slot.objects.all()
qs = qs.filter(day=self.day)
return qs
def rooms(self):
qs = Room.objects.all()
qs = qs.filter(schedule=self.day.schedule)
qs = qs.filter(
pk__in=SlotRoom.objects.filter(slot__in=self.slots_qs().values("pk")).values("room"))
qs = qs.order_by("order")
return qs
def __iter__(self):
times = sorted(set(itertools.chain(*self.slots_qs().values_list("start", "end"))))
slots = Slot.objects.filter(pk__in=self.slots_qs().values("pk"))
slots = slots.annotate(room_count=Count("slotroom"), order=Min("slotroom__room__order"))
slots = slots.order_by("start", "order")
row = []
total_room_count = self.rooms().count()
for time, next_time in pairwise(times):
row = {"time": time, "slots": []}
for slot in slots:
if slot.start == time:
slot.rowspan = TimeTable.rowspan(times, slot.start, slot.end)
slot.colspan = slot.room_count if not slot.exclusive else total_room_count
row["slots"].append(slot)
if row["slots"] or next_time is None:
yield row
@staticmethod
def rowspan(times, start, end):
return times.index(end) - times.index(start)
def pairwise(iterable):
a, b = itertools.tee(iterable)
b.next()
return itertools.izip_longest(a, b)

34
vendor/symposion/schedule/urls.py vendored Normal file
View file

@ -0,0 +1,34 @@
from django.conf.urls import url
from .views import (
schedule_conference,
schedule_edit,
schedule_list,
schedule_list_csv,
schedule_presentation_detail,
schedule_detail,
schedule_slot_edit,
schedule_json,
session_staff_email,
session_list,
session_detail,
EventFeed
)
urlpatterns = [
url(r"^$", schedule_conference, name="schedule_conference"),
url(r"^edit/$", schedule_edit, name="schedule_edit"),
url(r"^list/$", schedule_list, name="schedule_list"),
url(r"^presentations.csv$", schedule_list_csv, name="schedule_list_csv"),
url(r"^presentation/(\d+)/$", schedule_presentation_detail, name="schedule_presentation_detail"),
url(r"^sessions/staff.txt$", session_staff_email, name="schedule_session_staff_email"),
url(r"^sessions/$", session_list, name="schedule_session_list"),
url(r"^session/(\d+)/$", session_detail, name="schedule_session_detail"),
url(r"^([\w\-]+)/$", schedule_detail, name="schedule_detail"),
url(r"^([\w\-]+)/edit/$", schedule_edit, name="schedule_edit"),
url(r"^([\w\-]+)/list/$", schedule_list, name="schedule_list"),
url(r"^([\w\-]+)/presentations.csv$", schedule_list_csv, name="schedule_list_csv"),
url(r"^([\w\-]+)/edit/slot/(\d+)/", schedule_slot_edit, name="schedule_slot_edit"),
url(r"^conference.json", schedule_json, name="schedule_json"),
url(r"^conference.ics", EventFeed(), name="ical_feed"),
]

399
vendor/symposion/schedule/views.py vendored Normal file
View file

@ -0,0 +1,399 @@
import json
import pytz
from django.core.urlresolvers import reverse
from django.http import Http404, HttpResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.template import loader, Context
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib import messages
from django.contrib.sites.models import Site
from django_ical.views import ICalFeed
from django.contrib.auth.decorators import login_required
from symposion.schedule.forms import SlotEditForm, ScheduleSectionForm
from symposion.schedule.models import Schedule, Day, Slot, Presentation, Session, SessionRole
from symposion.schedule.timetable import TimeTable
from symposion.conference.models import Conference
def fetch_schedule(slug):
qs = Schedule.objects.all()
if slug is None:
if qs.count() > 1:
raise Http404()
schedule = next(iter(qs), None)
if schedule is None:
raise Http404()
else:
schedule = get_object_or_404(qs, section__slug=slug)
return schedule
def schedule_conference(request):
if request.user.is_staff:
schedules = Schedule.objects.filter(hidden=False)
else:
schedules = Schedule.objects.filter(published=True, hidden=False)
sections = []
for schedule in schedules:
days_qs = Day.objects.filter(schedule=schedule)
days = [TimeTable(day) for day in days_qs]
sections.append({
"schedule": schedule,
"days": days,
})
day_switch = request.GET.get('day', None)
ctx = {
"sections": sections,
"day_switch": day_switch
}
return render(request, "symposion/schedule/schedule_conference.html", ctx)
def schedule_detail(request, slug=None):
schedule = fetch_schedule(slug)
if not schedule.published and not request.user.is_staff:
raise Http404()
days_qs = Day.objects.filter(schedule=schedule)
days = [TimeTable(day) for day in days_qs]
ctx = {
"schedule": schedule,
"days": days,
}
return render(request, "symposion/schedule/schedule_detail.html", ctx)
def schedule_list(request, slug=None):
schedule = fetch_schedule(slug)
if not schedule.published and not request.user.is_staff:
raise Http404()
presentations = Presentation.objects.filter(section=schedule.section)
presentations = presentations.exclude(cancelled=True)
if not request.user.is_staff:
presentations = presentations.exclude(unpublish=True)
ctx = {
"schedule": schedule,
"presentations": presentations,
}
return render(request, "symposion/schedule/schedule_list.html", ctx)
def schedule_list_csv(request, slug=None):
schedule = fetch_schedule(slug)
if not schedule.published and not request.user.is_staff:
raise Http404()
presentations = Presentation.objects.filter(section=schedule.section)
presentations = presentations.exclude(cancelled=True)
if not request.user.is_staff:
presentations = presentations.exclude(unpublish=True)
presentations = presentations.order_by("id")
response = HttpResponse(content_type="text/csv")
if slug:
file_slug = slug
else:
file_slug = "presentations"
response["Content-Disposition"] = 'attachment; filename="%s.csv"' % file_slug
response.write(loader.get_template("symposion/schedule/schedule_list.csv").render(Context({
"presentations": presentations,
})))
return response
@login_required
def schedule_edit(request, slug=None):
if not request.user.is_staff:
raise Http404()
schedule = fetch_schedule(slug)
if request.method == "POST":
form = ScheduleSectionForm(
request.POST, request.FILES, schedule=schedule
)
if form.is_valid():
if 'submit' in form.data:
msg = form.build_schedule()
elif 'delete' in form.data:
msg = form.delete_schedule()
messages.add_message(request, msg[0], msg[1])
else:
form = ScheduleSectionForm(schedule=schedule)
days_qs = Day.objects.filter(schedule=schedule)
days = [TimeTable(day) for day in days_qs]
ctx = {
"schedule": schedule,
"days": days,
"form": form
}
return render(request, "symposion/schedule/schedule_edit.html", ctx)
@login_required
def schedule_slot_edit(request, slug, slot_pk):
if not request.user.is_staff:
raise Http404()
slot = get_object_or_404(Slot, day__schedule__section__slug=slug, pk=slot_pk)
if request.method == "POST":
form = SlotEditForm(request.POST, slot=slot)
if form.is_valid():
save = False
if "content_override" in form.cleaned_data:
slot.content_override = form.cleaned_data["content_override"]
save = True
if "presentation" in form.cleaned_data:
presentation = form.cleaned_data["presentation"]
if presentation is None:
slot.unassign()
else:
slot.assign(presentation)
if save:
slot.save()
return redirect("schedule_edit", slug)
else:
form = SlotEditForm(slot=slot)
ctx = {
"slug": slug,
"form": form,
"slot": slot,
}
return render(request, "symposion/schedule/_slot_edit.html", ctx)
def schedule_presentation_detail(request, pk):
presentation = get_object_or_404(Presentation, pk=pk)
if presentation.slot:
# 1) Schedule from presentation's slot
schedule = presentation.slot.day.schedule
else:
# 2) Fall back to the schedule for this proposal
schedule = presentation.proposal.kind.section.schedule
if not request.user.is_staff:
# 3) Is proposal unpublished?
if presentation.unpublish or not (schedule and schedule.published):
raise Http404()
ctx = {
"presentation": presentation,
"schedule": schedule,
}
return render(request, "symposion/schedule/presentation_detail.html", ctx)
def schedule_json(request):
slots = Slot.objects.filter(
day__schedule__published=True,
day__schedule__hidden=False
).order_by("start")
protocol = request.META.get('HTTP_X_FORWARDED_PROTO', 'http')
data = []
for slot in slots:
slot_data = {
"room": ", ".join(room["name"] for room in slot.rooms.values()),
"rooms": [room["name"] for room in slot.rooms.values()],
"start": slot.start_datetime.isoformat(),
"end": slot.end_datetime.isoformat(),
"duration": slot.length_in_minutes,
"kind": slot.kind.label,
"section": slot.day.schedule.section.slug,
"conf_key": slot.pk,
# TODO: models should be changed.
# these are model features from other conferences that have forked symposion
# these have been used almost everywhere and are good candidates for
# base proposals
"license": "CC BY",
"tags": "",
"released": False,
"contact": [],
}
if hasattr(slot.content, "proposal"):
if slot.content.unpublish and not request.user.is_staff:
continue
slot_data.update({
"name": slot.content.title,
"authors": [s.name for s in slot.content.speakers()],
"contact": [
s.email for s in slot.content.speakers()
] if request.user.has_perm('symposion_speakers.can_view_contact_details') or request.user.is_staff else ["redacted"],
"abstract": slot.content.abstract,
"conf_url": "%s://%s%s" % (
protocol,
Site.objects.get_current().domain,
reverse("schedule_presentation_detail", args=[slot.content.pk])
),
"cancelled": slot.content.cancelled,
"released": slot.content.proposal.recording_release
})
if not slot.content.speaker.twitter_username == '':
slot_data["twitter_id"] = slot.content.speaker.twitter_username
else:
slot_data.update({
"name": slot.content_override if slot.content_override else "Slot",
})
data.append(slot_data)
return HttpResponse(
json.dumps({"schedule": data}, indent=2),
content_type="application/json"
)
class EventFeed(ICalFeed):
product_id = '-//linux.conf.au/schedule//EN'
timezone = settings.TIME_ZONE
filename = 'conference.ics'
def description(self):
return Conference.objects.all().first().title
def items(self):
return Slot.objects.filter(
day__schedule__published=True,
day__schedule__hidden=False
).exclude(
kind__label='shortbreak'
).order_by("start")
def item_title(self, item):
if hasattr(item.content, 'proposal'):
title = item.content.title
else:
title = item.kind if item.kind else "Slot"
return title
def item_description(self, item):
if hasattr(item.content, 'proposal'):
description = "Speaker: %s\n%s" % (
item.content.speaker, item.content.abstract)
else:
description = item.content_override if item.content_override else "No description"
return description
def item_start_datetime(self, item):
return pytz.timezone(settings.TIME_ZONE).localize(item.start_datetime)
def item_end_datetime(self, item):
return pytz.timezone(settings.TIME_ZONE).localize(item.end_datetime)
def item_location(self, item):
return ", ".join(room["name"] for room in item.rooms.values())
def item_link(self, item) -> str:
if hasattr(item.content, 'proposal'):
return (
'http://%s%s' % (Site.objects.get_current().domain,
reverse('schedule_presentation_detail', args=[item.content.pk])))
else:
return 'http://%s' % Site.objects.get_current().domain
def item_guid(self, item):
return '%d@%s' % (item.pk, Site.objects.get_current().domain)
def session_list(request):
sessions = Session.objects.all().order_by('pk')
return render(request, "symposion/schedule/session_list.html", {
"sessions": sessions,
})
@login_required
def session_staff_email(request):
if not request.user.is_staff:
return redirect("schedule_session_list")
data = "\n".join(user.email for user in User.objects.filter(sessionrole__isnull=False).distinct())
return HttpResponse(data, content_type="text/plain;charset=UTF-8")
def session_detail(request, session_id):
session = get_object_or_404(Session, id=session_id)
chair = None
chair_denied = False
chairs = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_CHAIR).exclude(status=False)
if chairs:
chair = chairs[0].user
else:
if request.user.is_authenticated():
# did the current user previously try to apply and got rejected?
if SessionRole.objects.filter(session=session, user=request.user, role=SessionRole.SESSION_ROLE_CHAIR, status=False):
chair_denied = True
runner = None
runner_denied = False
runners = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_RUNNER).exclude(status=False)
if runners:
runner = runners[0].user
else:
if request.user.is_authenticated():
# did the current user previously try to apply and got rejected?
if SessionRole.objects.filter(session=session, user=request.user, role=SessionRole.SESSION_ROLE_RUNNER, status=False):
runner_denied = True
if request.method == "POST" and request.user.is_authenticated():
if not hasattr(request.user, "attendee") or not request.user.attendee.completed_registration:
response = redirect("guided_registration")
response["Location"] += "?next=%s" % request.path
return response
role = request.POST.get("role")
if role == "chair":
if chair is None and not chair_denied:
SessionRole(session=session, role=SessionRole.SESSION_ROLE_CHAIR, user=request.user).save()
elif role == "runner":
if runner is None and not runner_denied:
SessionRole(session=session, role=SessionRole.SESSION_ROLE_RUNNER, user=request.user).save()
elif role == "un-chair":
if chair == request.user:
session_role = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_CHAIR, user=request.user)
if session_role:
session_role[0].delete()
elif role == "un-runner":
if runner == request.user:
session_role = SessionRole.objects.filter(session=session, role=SessionRole.SESSION_ROLE_RUNNER, user=request.user)
if session_role:
session_role[0].delete()
return redirect("schedule_session_detail", session_id)
return render(request, "symposion/schedule/session_detail.html", {
"session": session,
"chair": chair,
"chair_denied": chair_denied,
"runner": runner,
"runner_denied": runner_denied,
})

1
vendor/symposion/speakers/__init__.py vendored Normal file
View file

@ -0,0 +1 @@
default_app_config = "symposion.speakers.apps.SpeakersConfig"

8
vendor/symposion/speakers/admin.py vendored Normal file
View file

@ -0,0 +1,8 @@
from django.contrib import admin
from symposion.speakers.models import Speaker
admin.site.register(Speaker,
list_display=["name", "email", "created", "twitter_username"],
search_fields=["name"])

8
vendor/symposion/speakers/apps.py vendored Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class SpeakersConfig(AppConfig):
name = "symposion.speakers"
label = "symposion_speakers"
verbose_name = _("Symposion Speakers")

32
vendor/symposion/speakers/forms.py vendored Normal file
View file

@ -0,0 +1,32 @@
from django import forms
from symposion.speakers.models import Speaker
class SpeakerForm(forms.ModelForm):
required_css_class = 'label-required'
class Meta:
model = Speaker
fields = [
"name",
"biography",
"experience",
"photo",
"telephone",
"homepage",
"twitter_username",
"accessibility",
"agreement",
]
def __init__(self, *a, **k):
super(SpeakerForm, self).__init__(*a, **k)
self.fields['agreement'].required = True
def clean_twitter_username(self):
value = self.cleaned_data["twitter_username"]
if value.startswith("@"):
value = value[1:]
return value

View file

View file

@ -0,0 +1,20 @@
import csv
import os
from django.core.management.base import BaseCommand
from symposion.speakers.models import Speaker
class Command(BaseCommand):
def handle(self, *args, **options):
with open(os.path.join(os.getcwd(), "speakers.csv"), "w") as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["Name", "Bio"])
for speaker in Speaker.objects.all():
csv_writer.writerow([
speaker.name,
speaker.biography,
])

View file

@ -0,0 +1,48 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-17 03:35
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Speaker',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='As you would like it to appear in the conference programme.', max_length=100, verbose_name='Name')),
('biography', models.TextField(blank=True, help_text="This will appear on the conference website and in the programme. Please write in the third person, eg 'Alice is a Moblin hacker...', 150-200 words. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>.", verbose_name='Biography')),
('biography_html', models.TextField(blank=True)),
('experience', models.TextField(blank=True, help_text="Have you had any experience presenting elsewhere? If so, we'd like to know. Anything you put here will only be seen by the organisers and reviewers; use it to convince them why they should accept your proposal. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>.", verbose_name='Speaking experience')),
('experience_html', models.TextField(blank=True)),
('photo', models.ImageField(blank=True, upload_to='speaker_photos', verbose_name='Photo')),
('telephone', models.CharField(help_text="The conference team will need this to contact you during the conference week. If you don't have one, or do not wish to provide it, then enter NONE in this field.", max_length=15)),
('homepage', models.URLField(blank=True, help_text='Your home page, if you have one')),
('twitter_username', models.CharField(blank=True, help_text='Your Twitter account', max_length=15)),
('accessibility', models.TextField(blank=True, help_text="Please describe any special accessibility requirements that you may have. Edit using <a href='http://warpedvisions.org/projects/markdown-cheat-sheet/' target='_blank'>Markdown</a>.", verbose_name='Accessibility requirements')),
('accessibility_html', models.TextField(blank=True)),
('travel_assistance', models.BooleanField(default=False, help_text='Check this box if you require assistance to travel to Hobart to present your proposed sessions.', verbose_name='Travel assistance required')),
('accommodation_assistance', models.BooleanField(default=False, help_text='Check this box if you require us to provide you with student-style accommodation in order to present your proposed sessions.', verbose_name='Accommodation assistance required')),
('agreement', models.BooleanField(default=False, help_text='I agree to the terms and conditions of attendance, and I have read, understood, and agree to act according to the standards set forth in our Code of Conduct ')),
('annotation', models.TextField(verbose_name='Annotation')),
('invite_email', models.CharField(db_index=True, max_length=200, null=True, unique=True, verbose_name='Invite_email')),
('invite_token', models.CharField(db_index=True, max_length=40, verbose_name='Invite token')),
('created', models.DateTimeField(default=datetime.datetime.now, editable=False, verbose_name='Created')),
('user', models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='speaker_profile', to=settings.AUTH_USER_MODEL, verbose_name='User')),
],
options={
'ordering': ['name'],
'verbose_name': 'Speaker',
'verbose_name_plural': 'Speakers',
},
),
]

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-12-30 08:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('symposion_speakers', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='speaker',
options={'ordering': ['name'], 'permissions': (('can_view_contact_details', 'Can View Contact Details'),), 'verbose_name': 'Speaker', 'verbose_name_plural': 'Speakers'},
),
]

View file

132
vendor/symposion/speakers/models.py vendored Normal file
View file

@ -0,0 +1,132 @@
import datetime
from django.core.urlresolvers import reverse
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User
from symposion import constants
from symposion.text_parser import parse
class Speaker(models.Model):
SESSION_COUNT_CHOICES = [
(1, "One"),
(2, "Two")
]
user = models.OneToOneField(User, null=True, related_name="speaker_profile", verbose_name=_("User"))
name = models.CharField(verbose_name=_("Name"), max_length=100,
help_text=_("As you would like it to appear in the"
" conference programme."))
biography = models.TextField(
blank=True,
help_text=_("This will appear on the conference website and in the "
"programme. Please write in the third person, eg 'Alice "
"is a Moblin hacker...', 150-200 words. " +
constants.TEXT_FIELD_MONOSPACE_NOTE),
verbose_name=_("Biography"),
)
biography_html = models.TextField(blank=True)
experience = models.TextField(
blank=True,
help_text=_("Have you had any experience presenting elsewhere? If so, "
"we'd like to know. Anything you put here will only be "
"seen by the organisers and reviewers; use it to convince "
"them why they should accept your proposal. " +
constants.TEXT_FIELD_MONOSPACE_NOTE),
verbose_name=_("Speaking experience"),
)
experience_html = models.TextField(blank=True)
photo = models.ImageField(upload_to="speaker_photos", blank=True, verbose_name=_("Photo"))
telephone = models.CharField(
max_length=15,
help_text=_(u"The conference team will need this to contact you "
"during the conference week. If you don't have one, or do "
"not wish to provide it, then enter NONE in this field.")
)
homepage = models.URLField(
blank=True,
help_text=_(u"Your home page, if you have one")
)
twitter_username = models.CharField(
max_length=15,
blank=True,
help_text=_(u"Your Twitter account")
)
accessibility = models.TextField(
blank=True,
help_text=_("Please describe any special accessibility requirements "
"that you may have. " +
constants.TEXT_FIELD_MONOSPACE_NOTE),
verbose_name=_("Accessibility requirements"))
accessibility_html = models.TextField(blank=True)
travel_assistance = models.BooleanField(
blank=True,
default=False,
help_text=_("Check this box if you require assistance to travel to Hobart to "
"present your proposed sessions."),
verbose_name=_("Travel assistance required"),
)
accommodation_assistance = models.BooleanField(
blank=True,
default=False,
help_text=_("Check this box if you require us to provide you with student-style "
"accommodation in order to present your proposed sessions."),
verbose_name=_("Accommodation assistance required"),
)
agreement = models.BooleanField(
default=False,
help_text=_("I agree to the terms and conditions of attendance, and "
"I have read, understood, and agree to act according to "
"the standards set forth in our Code of Conduct ")
)
annotation = models.TextField(verbose_name=_("Annotation")) # staff only
invite_email = models.CharField(max_length=200, unique=True, null=True, db_index=True, verbose_name=_("Invite_email"))
invite_token = models.CharField(max_length=40, db_index=True, verbose_name=_("Invite token"))
created = models.DateTimeField(
default=datetime.datetime.now,
editable=False,
verbose_name=_("Created")
)
class Meta:
ordering = ['name']
verbose_name = _("Speaker")
verbose_name_plural = _("Speakers")
permissions = (('can_view_contact_details', 'Can View Contact Details'),)
def save(self, *args, **kwargs):
self.biography_html = parse(self.biography)
self.experience_html = parse(self.experience)
self.accessibility_html = parse(self.accessibility)
return super(Speaker, self).save(*args, **kwargs)
def __str__(self):
if self.user:
return self.name
else:
return "?"
def get_absolute_url(self):
return reverse("speaker_edit")
@property
def email(self):
if self.user is not None:
return self.user.email
else:
return self.invite_email
@property
def all_presentations(self):
presentations = []
if self.presentations:
for p in self.presentations.all():
presentations.append(p)
for p in self.copresentations.all():
presentations.append(p)
return presentations

17
vendor/symposion/speakers/urls.py vendored Normal file
View file

@ -0,0 +1,17 @@
from django.conf.urls import url
from .views import (
speaker_create,
speaker_create_token,
speaker_edit,
speaker_profile,
speaker_create_staff
)
urlpatterns = [
url(r"^create/$", speaker_create, name="speaker_create"),
url(r"^create/(\w+)/$", speaker_create_token, name="speaker_create_token"),
url(r"^edit/(?:(?P<pk>\d+)/)?$", speaker_edit, name="speaker_edit"),
url(r"^profile/(?P<pk>\d+)/$", speaker_profile, name="speaker_profile"),
url(r"^staff/create/(\d+)/$", speaker_create_staff, name="speaker_create_staff"),
]

133
vendor/symposion/speakers/views.py vendored Normal file
View file

@ -0,0 +1,133 @@
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _
from symposion.proposals.models import ProposalBase
from symposion.speakers.forms import SpeakerForm
from symposion.speakers.models import Speaker
@login_required
def speaker_create(request):
try:
return redirect(request.user.speaker_profile)
except ObjectDoesNotExist:
pass
if request.method == "POST":
try:
speaker = Speaker.objects.get(invite_email=request.user.email)
found = True
except Speaker.DoesNotExist:
speaker = None
found = False
form = SpeakerForm(request.POST, request.FILES, instance=speaker)
if form.is_valid():
speaker = form.save(commit=False)
speaker.user = request.user
if not found:
speaker.invite_email = None
speaker.save()
messages.success(request, _("Speaker profile created."))
return redirect("dashboard")
else:
form = SpeakerForm(initial={"name": request.user.get_full_name()})
return render(request, "symposion/speakers/speaker_create.html", {
"speaker_form": form,
})
@login_required
def speaker_create_staff(request, pk):
user = get_object_or_404(User, pk=pk)
if not request.user.is_staff:
raise Http404
try:
return redirect(user.speaker_profile)
except ObjectDoesNotExist:
pass
if request.method == "POST":
form = SpeakerForm(request.POST, request.FILES)
if form.is_valid():
speaker = form.save(commit=False)
speaker.user = user
speaker.save()
messages.success(request, _("Speaker profile created."))
return redirect("user_list")
else:
form = SpeakerForm(initial={"name": user.get_full_name()})
return render(request, "symposion/speakers/speaker_create.html", {
"speaker_form": form,
})
@login_required
def speaker_create_token(request, token):
speaker = get_object_or_404(Speaker, invite_token=token)
request.session["pending-token"] = token
# check for speaker profile
try:
existing_speaker = request.user.speaker_profile
except ObjectDoesNotExist:
pass
else:
del request.session["pending-token"]
additional_speakers = ProposalBase.additional_speakers.through
additional_speakers._default_manager.filter(
speaker=speaker
).update(
speaker=existing_speaker
)
messages.info(request, _("You have been associated with all pending "
"talk proposals"))
return redirect("dashboard")
return redirect("speaker_create")
@login_required
def speaker_edit(request, pk=None):
if pk is None:
try:
speaker = request.user.speaker_profile
except Speaker.DoesNotExist:
return redirect("speaker_create")
else:
if request.user.is_staff:
speaker = get_object_or_404(Speaker, pk=pk)
else:
raise Http404()
if request.method == "POST":
form = SpeakerForm(request.POST, request.FILES, instance=speaker)
if form.is_valid():
form.save()
messages.success(request, "Speaker profile updated.")
return redirect("dashboard")
else:
form = SpeakerForm(instance=speaker)
return render(request, "symposion/speakers/speaker_edit.html", {
"speaker_form": form,
})
def speaker_profile(request, pk):
speaker = get_object_or_404(Speaker, pk=pk)
presentations = speaker.all_presentations
if not presentations and not request.user.is_staff:
raise Http404()
return render(request, "symposion/speakers/speaker_profile.html", {
"speaker": speaker,
"presentations": presentations,
})

View file

@ -0,0 +1 @@
default_app_config = "symposion.sponsorship.apps.SponsorshipConfig"

128
vendor/symposion/sponsorship/admin.py vendored Normal file
View file

@ -0,0 +1,128 @@
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _
from django.contrib import admin
from symposion.sponsorship.models import (
Benefit,
BENEFITS,
BenefitLevel,
Sponsor,
SponsorBenefit,
SponsorLevel,
)
class BenefitLevelInline(admin.TabularInline):
model = BenefitLevel
extra = 0
class SponsorBenefitInline(admin.StackedInline):
model = SponsorBenefit
extra = 0
fieldsets = [
(None, {
"fields": [
("benefit", "active"),
("max_words", "other_limits"),
"text",
"upload",
]
})
]
class SponsorAdmin(admin.ModelAdmin):
save_on_top = True
fieldsets = [
(None, {
"fields": [
("name", "applicant"),
("level", "active"),
"external_url",
"annotation",
("contact_name", "contact_email")
]
}),
("Metadata", {
"fields": ["added"],
"classes": ["collapse"]
})
]
inlines = [SponsorBenefitInline]
list_filter = ["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):
# @@@ kinda ugly but using choices= on NullBooleanField is broken
form = super(SponsorAdmin, self).get_form(*args, **kwargs)
form.base_fields["active"].widget.choices = [
("1", _("unreviewed")),
("2", _("approved")),
("3", _("rejected"))
]
return form
# Define accessor functions for our benefit fields and add them to
# list_display, so we can sort on them and give them sensible names.
# Add the fields to list_filters while we're at it.
for benefit in BENEFITS:
benefit_name = benefit['name']
field_name = benefit['field_name']
def func_generator(ben):
def column_func(obj):
return getattr(obj, ben['field_name'])
column_func.short_description = ben['column_title']
column_func.boolean = True
column_func.admin_order_field = ben['field_name']
return column_func
list_display.append(func_generator(benefit))
list_filter.append(field_name)
def save_related(self, request, form, formsets, change):
super(SponsorAdmin, self).save_related(request, form, formsets, change)
obj = form.instance
obj.save()
class BenefitAdmin(admin.ModelAdmin):
list_display = ["name", "type", "description", "levels"]
inlines = [BenefitLevelInline]
def levels(self, benefit):
return u", ".join(l.level.name for l in benefit.benefit_levels.all())
class SponsorLevelAdmin(admin.ModelAdmin):
inlines = [BenefitLevelInline]
class SponsorBenefitAdmin(admin.ModelAdmin):
list_display = ('benefit', 'sponsor', 'active', '_is_complete', 'show_text')
def show_text(self, sponsor_benefit):
if sponsor_benefit.text:
return sponsor_benefit.text[:100]
else:
return _("None")
admin.site.register(SponsorLevel, SponsorLevelAdmin)
admin.site.register(Sponsor, SponsorAdmin)
admin.site.register(Benefit, BenefitAdmin)
admin.site.register(SponsorBenefit, SponsorBenefitAdmin)

8
vendor/symposion/sponsorship/apps.py vendored Normal file
View file

@ -0,0 +1,8 @@
from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
class SponsorshipConfig(AppConfig):
name = "symposion.sponsorship"
label = "symposion_sponsorship"
verbose_name = _("Symposion Sponsorship")

91
vendor/symposion/sponsorship/forms.py vendored Normal file
View file

@ -0,0 +1,91 @@
from django import forms
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.admin.widgets import AdminFileWidget
from django.utils.translation import ugettext_lazy as _
from symposion.sponsorship.models import Sponsor, SponsorBenefit
class SponsorApplicationForm(forms.ModelForm):
required_css_class = 'label-required'
def __init__(self, *args, **kwargs):
self.user = kwargs.pop("user")
kwargs.update({
"initial": {
"contact_name": self.user.get_full_name,
"contact_email": self.user.email,
}
})
super(SponsorApplicationForm, self).__init__(*args, **kwargs)
class Meta:
model = Sponsor
fields = [
"name",
"external_url",
"contact_name",
"contact_email",
"level"
]
def save(self, commit=True):
obj = super(SponsorApplicationForm, self).save(commit=False)
obj.applicant = self.user
if commit:
obj.save()
return obj
class SponsorDetailsForm(forms.ModelForm):
required_css_class = 'label-required'
class Meta:
model = Sponsor
fields = [
"name",
"external_url",
"contact_name",
"contact_email"
]
class SponsorBenefitsInlineFormSet(BaseInlineFormSet):
required_css_class = 'label-required'
def __init__(self, *args, **kwargs):
kwargs['queryset'] = kwargs.get('queryset', self.model._default_manager).exclude(benefit__type="option")
super(SponsorBenefitsInlineFormSet, self).__init__(*args, **kwargs)
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 = _("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"]
)

View file

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
with open(os.path.join(os.getcwd(), "build", "sponsors.csv"), "w") as csv_file:
csv_writer = csv.writer(csv_file)
csv_writer.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_writer.writerow([
data["name"],
data["url"],
data["level"],
data["description"]
])
zipdir(
os.path.join(os.getcwd(), "build"),
os.path.join(os.getcwd(), "sponsors.zip"))

View file

@ -0,0 +1,36 @@
from django.core.management.base import BaseCommand
from symposion.sponsorship.models import Sponsor, SponsorBenefit, SponsorLevel
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 %s for %s" % (sponsor_benefit, 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()

View file

@ -0,0 +1,7 @@
from django.db import models
class SponsorManager(models.Manager):
def active(self):
return self.get_query_set().filter(active=True).order_by("level")

View file

@ -0,0 +1,117 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.7 on 2016-09-17 03:35
import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('symposion_conference', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Benefit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Name')),
('description', models.TextField(blank=True, verbose_name='Description')),
('type', models.CharField(choices=[('text', 'Text'), ('file', 'File'), ('richtext', 'Rich Text'), ('weblogo', 'Web Logo'), ('simple', 'Simple'), ('option', 'Option')], default='simple', max_length=10, verbose_name='Type')),
('content_type', models.CharField(choices=[('simple', 'Simple'), ('listing_text_af', 'Listing Text (Afrikaans)'), ('listing_text_ar', 'Listing Text (Arabic)'), ('listing_text_ast', 'Listing Text (Asturian)'), ('listing_text_az', 'Listing Text (Azerbaijani)'), ('listing_text_bg', 'Listing Text (Bulgarian)'), ('listing_text_be', 'Listing Text (Belarusian)'), ('listing_text_bn', 'Listing Text (Bengali)'), ('listing_text_br', 'Listing Text (Breton)'), ('listing_text_bs', 'Listing Text (Bosnian)'), ('listing_text_ca', 'Listing Text (Catalan)'), ('listing_text_cs', 'Listing Text (Czech)'), ('listing_text_cy', 'Listing Text (Welsh)'), ('listing_text_da', 'Listing Text (Danish)'), ('listing_text_de', 'Listing Text (German)'), ('listing_text_el', 'Listing Text (Greek)'), ('listing_text_en', 'Listing Text (English)'), ('listing_text_en-au', 'Listing Text (Australian English)'), ('listing_text_en-gb', 'Listing Text (British English)'), ('listing_text_eo', 'Listing Text (Esperanto)'), ('listing_text_es', 'Listing Text (Spanish)'), ('listing_text_es-ar', 'Listing Text (Argentinian Spanish)'), ('listing_text_es-co', 'Listing Text (Colombian Spanish)'), ('listing_text_es-mx', 'Listing Text (Mexican Spanish)'), ('listing_text_es-ni', 'Listing Text (Nicaraguan Spanish)'), ('listing_text_es-ve', 'Listing Text (Venezuelan Spanish)'), ('listing_text_et', 'Listing Text (Estonian)'), ('listing_text_eu', 'Listing Text (Basque)'), ('listing_text_fa', 'Listing Text (Persian)'), ('listing_text_fi', 'Listing Text (Finnish)'), ('listing_text_fr', 'Listing Text (French)'), ('listing_text_fy', 'Listing Text (Frisian)'), ('listing_text_ga', 'Listing Text (Irish)'), ('listing_text_gd', 'Listing Text (Scottish Gaelic)'), ('listing_text_gl', 'Listing Text (Galician)'), ('listing_text_he', 'Listing Text (Hebrew)'), ('listing_text_hi', 'Listing Text (Hindi)'), ('listing_text_hr', 'Listing Text (Croatian)'), ('listing_text_hu', 'Listing Text (Hungarian)'), ('listing_text_ia', 'Listing Text (Interlingua)'), ('listing_text_id', 'Listing Text (Indonesian)'), ('listing_text_io', 'Listing Text (Ido)'), ('listing_text_is', 'Listing Text (Icelandic)'), ('listing_text_it', 'Listing Text (Italian)'), ('listing_text_ja', 'Listing Text (Japanese)'), ('listing_text_ka', 'Listing Text (Georgian)'), ('listing_text_kk', 'Listing Text (Kazakh)'), ('listing_text_km', 'Listing Text (Khmer)'), ('listing_text_kn', 'Listing Text (Kannada)'), ('listing_text_ko', 'Listing Text (Korean)'), ('listing_text_lb', 'Listing Text (Luxembourgish)'), ('listing_text_lt', 'Listing Text (Lithuanian)'), ('listing_text_lv', 'Listing Text (Latvian)'), ('listing_text_mk', 'Listing Text (Macedonian)'), ('listing_text_ml', 'Listing Text (Malayalam)'), ('listing_text_mn', 'Listing Text (Mongolian)'), ('listing_text_mr', 'Listing Text (Marathi)'), ('listing_text_my', 'Listing Text (Burmese)'), ('listing_text_nb', 'Listing Text (Norwegian Bokmal)'), ('listing_text_ne', 'Listing Text (Nepali)'), ('listing_text_nl', 'Listing Text (Dutch)'), ('listing_text_nn', 'Listing Text (Norwegian Nynorsk)'), ('listing_text_os', 'Listing Text (Ossetic)'), ('listing_text_pa', 'Listing Text (Punjabi)'), ('listing_text_pl', 'Listing Text (Polish)'), ('listing_text_pt', 'Listing Text (Portuguese)'), ('listing_text_pt-br', 'Listing Text (Brazilian Portuguese)'), ('listing_text_ro', 'Listing Text (Romanian)'), ('listing_text_ru', 'Listing Text (Russian)'), ('listing_text_sk', 'Listing Text (Slovak)'), ('listing_text_sl', 'Listing Text (Slovenian)'), ('listing_text_sq', 'Listing Text (Albanian)'), ('listing_text_sr', 'Listing Text (Serbian)'), ('listing_text_sr-latn', 'Listing Text (Serbian Latin)'), ('listing_text_sv', 'Listing Text (Swedish)'), ('listing_text_sw', 'Listing Text (Swahili)'), ('listing_text_ta', 'Listing Text (Tamil)'), ('listing_text_te', 'Listing Text (Telugu)'), ('listing_text_th', 'Listing Text (Thai)'), ('listing_text_tr', 'Listing Text (Turkish)'), ('listing_text_tt', 'Listing Text (Tatar)'), ('listing_text_udm', 'Listing Text (Udmurt)'), ('listing_text_uk', 'Listing Text (Ukrainian)'), ('listing_text_ur', 'Listing Text (Urdu)'), ('listing_text_vi', 'Listing Text (Vietnamese)'), ('listing_text_zh-hans', 'Listing Text (Simplified Chinese)'), ('listing_text_zh-hant', 'Listing Text (Traditional Chinese)')], default='simple', max_length=20, verbose_name='content type')),
],
),
migrations.CreateModel(
name='BenefitLevel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('max_words', models.PositiveIntegerField(blank=True, null=True, verbose_name='Max words')),
('other_limits', models.CharField(blank=True, max_length=200, verbose_name='Other limits')),
('benefit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='benefit_levels', to='symposion_sponsorship.Benefit', verbose_name='Benefit')),
],
options={
'ordering': ['level'],
'verbose_name': 'Benefit level',
'verbose_name_plural': 'Benefit levels',
},
),
migrations.CreateModel(
name='Sponsor',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Sponsor Name')),
('display_url', models.URLField(blank=True, verbose_name='display URL')),
('external_url', models.URLField(verbose_name='External URL')),
('annotation', models.TextField(blank=True, verbose_name='Annotation')),
('contact_name', models.CharField(max_length=100, verbose_name='Contact Name')),
('contact_email', models.EmailField(max_length=254, verbose_name='Contact Email')),
('added', models.DateTimeField(default=datetime.datetime.now, verbose_name='added')),
('active', models.BooleanField(default=False, verbose_name='active')),
('web_logo_benefit', models.NullBooleanField(help_text='Web logo benefit is complete', verbose_name='Web logo benefit')),
('print_logo_benefit', models.NullBooleanField(help_text='Print logo benefit is complete', verbose_name='Print logo benefit')),
('print_description_benefit', models.NullBooleanField(help_text='Print description benefit is complete', verbose_name='Print description benefit')),
('company_description_benefit', models.NullBooleanField(help_text='Company description benefit is complete', verbose_name='Company description benefit')),
('applicant', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sponsorships', to=settings.AUTH_USER_MODEL, verbose_name='Applicant')),
],
options={
'ordering': ['name'],
'verbose_name': 'Sponsor',
'verbose_name_plural': 'Sponsors',
},
),
migrations.CreateModel(
name='SponsorBenefit',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('active', models.BooleanField(default=True, verbose_name='Active')),
('max_words', models.PositiveIntegerField(blank=True, null=True, verbose_name='Max words')),
('other_limits', models.CharField(blank=True, max_length=200, verbose_name='Other limits')),
('text', models.TextField(blank=True, verbose_name='Text')),
('upload', models.FileField(blank=True, upload_to='sponsor_files', verbose_name='File')),
('is_complete', models.NullBooleanField(help_text='True - benefit complete; False - benefit incomplete; Null - n/a', verbose_name='Complete?')),
('benefit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sponsor_benefits', to='symposion_sponsorship.Benefit', verbose_name='Benefit')),
('sponsor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sponsor_benefits', to='symposion_sponsorship.Sponsor', verbose_name='Sponsor')),
],
options={
'ordering': ['-active'],
'verbose_name': 'Sponsor benefit',
'verbose_name_plural': 'Sponsor benefits',
},
),
migrations.CreateModel(
name='SponsorLevel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Name')),
('order', models.IntegerField(default=0, verbose_name='Order')),
('cost', models.PositiveIntegerField(verbose_name='Cost')),
('description', models.TextField(blank=True, help_text='This is private.', verbose_name='Description')),
('conference', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_conference.Conference', verbose_name='Conference')),
],
options={
'ordering': ['conference', 'order'],
'verbose_name': 'Sponsor level',
'verbose_name_plural': 'Sponsor levels',
},
),
migrations.AddField(
model_name='sponsor',
name='level',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='symposion_sponsorship.SponsorLevel', verbose_name='level'),
),
migrations.AddField(
model_name='sponsor',
name='sponsor_logo',
field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='+', to='symposion_sponsorship.SponsorBenefit', verbose_name='Sponsor logo'),
),
migrations.AddField(
model_name='benefitlevel',
name='level',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='benefit_levels', to='symposion_sponsorship.SponsorLevel', verbose_name='Level'),
),
]

View file

331
vendor/symposion/sponsorship/models.py vendored Normal file
View file

@ -0,0 +1,331 @@
import datetime
from django.conf import settings
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 symposion.sponsorship.managers import SponsorManager
# The benefits we track as individual fields on sponsors
# Names are the names in the database as defined by organizers.
# Field names are the benefit names, lowercased, with
# spaces changed to _, and with "_benefit" appended.
# Column titles are arbitrary.
# "really just care about the ones we have today: print logo, web logo, print description, web description and the ad."
BENEFITS = [
{
'name': 'Web logo',
'field_name': 'web_logo_benefit',
'column_title': _(u"Web Logo"),
}, {
'name': 'Print logo',
'field_name': 'print_logo_benefit',
'column_title': _(u"Print Logo"),
}, {
'name': 'Company Description',
'field_name': 'company_description_benefit',
'column_title': _(u"Web Desc"),
}, {
'name': 'Print Description',
'field_name': 'print_description_benefit',
'column_title': _(u"Print Desc"),
}
]
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 __str__(self):
return "%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)
display_url = models.URLField(_("display URL"), blank=True)
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(_("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, verbose_name=_("Sponsor logo"))
# Whether things are complete
# True = complete, False = incomplate, Null = n/a for this sponsor level
web_logo_benefit = models.NullBooleanField(_("Web logo benefit"), help_text=_(u"Web logo benefit is complete"))
print_logo_benefit = models.NullBooleanField(_("Print logo benefit"), help_text=_(u"Print logo benefit is complete"))
print_description_benefit = models.NullBooleanField(_("Print description benefit"), help_text=_(u"Print description benefit is complete"))
company_description_benefit = models.NullBooleanField(_("Company description benefit"), help_text=_(u"Company description benefit is complete"))
objects = SponsorManager()
def __str__(self):
return self.name
class Meta:
verbose_name = _("Sponsor")
verbose_name_plural = _("Sponsors")
ordering = ['name']
def save(self, *args, **kwargs):
# Set fields related to benefits being complete
for benefit in BENEFITS:
field_name = benefit['field_name']
benefit_name = benefit['name']
setattr(self, field_name, self.benefit_is_complete(benefit_name))
super(Sponsor, self).save(*args, **kwargs)
def get_absolute_url(self):
if self.active:
return reverse("sponsor_detail", kwargs={"pk": self.pk})
return reverse("sponsor_list")
def get_display_url(self):
if self.display_url:
return self.display_url
else:
return self.external_url
@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
@property
def listing_text(self):
if not hasattr(self, "_listing_text"):
self._listing_text = ""
# @@@ better than hard-coding a pk but still not good
benefits = self.sponsor_benefits.filter(benefit__name="Sponsor Description")
if benefits.count():
self._listing_text = benefits[0].text
return self._listing_text
def reset_benefits(self):
"""
Reset all benefits for this sponsor to the defaults for their
sponsorship level.
"""
level = None
try:
level = self.level
except SponsorLevel.DoesNotExist:
pass
allowed_benefits = []
if level:
for benefit_level in level.benefit_levels.all():
# Create all needed benefits if they don't exist already
sponsor_benefit, created = SponsorBenefit.objects.get_or_create(
sponsor=self, benefit=benefit_level.benefit)
# and set to default limits for this level.
sponsor_benefit.max_words = benefit_level.max_words
sponsor_benefit.other_limits = benefit_level.other_limits
# and set to active
sponsor_benefit.active = True
# @@@ We don't call sponsor_benefit.clean here. This means
# that if the sponsorship level for a sponsor is adjusted
# downwards, an existing too-long text entry can remain,
# and won't raise a validation error until it's next
# edited.
sponsor_benefit.save()
allowed_benefits.append(sponsor_benefit.pk)
# Any remaining sponsor benefits that don't normally belong to
# this level are set to inactive
self.sponsor_benefits.exclude(pk__in=allowed_benefits)\
.update(active=False, max_words=None, other_limits="")
def send_coordinator_emails(self):
pass # @@@ should this just be done centrally?
def benefit_is_complete(self, name):
"""Return True - benefit is complete, False - benefit is not complete,
or None - benefit not applicable for this sponsor's level """
if BenefitLevel.objects.filter(level=self.level, benefit__name=name).exists():
try:
benefit = self.sponsor_benefits.get(benefit__name=name)
except SponsorBenefit.DoesNotExist:
return False
else:
return benefit.is_complete
else:
return None # Not an applicable benefit for this sponsor's level
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)
BENEFIT_TYPE_CHOICES = [
("text", _("Text")),
("file", _("File")),
("richtext", _("Rich Text")),
("weblogo", _("Web Logo")),
("simple", _("Simple")),
("option", _("Option"))
]
CONTENT_TYPE_CHOICES = [
("simple", "Simple"),
] + [
("listing_text_%s" % lang, "Listing Text (%s)" % label) for lang, label in settings.LANGUAGES
]
class Benefit(models.Model):
name = models.CharField(_("Name"), max_length=100)
description = models.TextField(_("Description"), blank=True)
type = models.CharField(_("Type"), choices=BENEFIT_TYPE_CHOICES, max_length=10,
default="simple")
content_type = models.CharField(_("content type"), choices=CONTENT_TYPE_CHOICES,
max_length=20, default="simple")
def __str__(self):
return self.name
class BenefitLevel(models.Model):
benefit = models.ForeignKey(Benefit, related_name="benefit_levels", verbose_name=_("Benefit"))
level = models.ForeignKey(SponsorLevel, related_name="benefit_levels", verbose_name=_("Level"))
# default limits for this benefit at given level
max_words = models.PositiveIntegerField(_("Max words"), blank=True, null=True)
other_limits = models.CharField(_("Other limits"), max_length=200, blank=True)
class Meta:
ordering = ["level"]
verbose_name = _("Benefit level")
verbose_name_plural = _("Benefit levels")
def __str__(self):
return "%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, verbose_name=_("Active"))
# 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")
# Whether any assets required from the sponsor have been provided
# (e.g. a logo file for a Web logo benefit).
is_complete = models.NullBooleanField(_("Complete?"), help_text=_(u"True - benefit complete; False - benefit incomplete; Null - n/a"))
class Meta:
ordering = ["-active"]
verbose_name = _("Sponsor benefit")
verbose_name_plural = _("Sponsor benefits")
def __str__(self):
return "%s - %s (%s)" % (self.sponsor, self.benefit, self.benefit.type)
def save(self, *args, **kwargs):
# Validate - save() doesn't clean your model by default, so call
# it explicitly before saving
self.full_clean()
self.is_complete = self._is_complete()
super(SponsorBenefit, self).save(*args, **kwargs)
def clean(self):
num_words = len(self.text.split())
if self.max_words and num_words > self.max_words:
raise ValidationError(
_("Sponsorship level only allows for %(word)s words, you provided %(num)d.") % {
"word": self.max_words, "num": num_words})
def data_fields(self):
"""
Return list of data field names which should be editable for
this ``SponsorBenefit``, depending on its ``Benefit`` type.
"""
if self.benefit.type == "file" or self.benefit.type == "weblogo":
return ["upload"]
elif self.benefit.type in ("text", "richtext", "simple", "option"):
return ["text"]
return []
def _is_text_benefit(self):
return self.benefit.type in ["text", "richtext", "simple"] and bool(self.text)
def _is_upload_benefit(self):
return self.benefit.type in ["file", "weblogo"] and bool(self.upload)
def _is_complete(self):
return self.active and (self._is_text_benefit() or self._is_upload_benefit())
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)

View file

Some files were not shown because too many files have changed in this diff Show more