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:
commit
2ad28ebf71
127 changed files with 11159 additions and 0 deletions
27
vendor/symposion/LICENSE
vendored
Normal file
27
vendor/symposion/LICENSE
vendored
Normal 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
1
vendor/symposion/__init__.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
__version__ = "1.0b2.dev3"
|
8
vendor/symposion/conf.py
vendored
Normal file
8
vendor/symposion/conf.py
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
from django.conf import settings # noqa
|
||||
|
||||
from appconf import AppConf
|
||||
|
||||
|
||||
class SymposionAppConf(AppConf):
|
||||
|
||||
VOTE_THRESHOLD = 3
|
1
vendor/symposion/conference/__init__.py
vendored
Normal file
1
vendor/symposion/conference/__init__.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = "symposion.conference.apps.ConferenceConfig"
|
22
vendor/symposion/conference/admin.py
vendored
Normal file
22
vendor/symposion/conference/admin.py
vendored
Normal 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
8
vendor/symposion/conference/apps.py
vendored
Normal 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")
|
46
vendor/symposion/conference/migrations/0001_initial.py
vendored
Normal file
46
vendor/symposion/conference/migrations/0001_initial.py
vendored
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
0
vendor/symposion/conference/migrations/__init__.py
vendored
Normal file
0
vendor/symposion/conference/migrations/__init__.py
vendored
Normal file
82
vendor/symposion/conference/models.py
vendored
Normal file
82
vendor/symposion/conference/models.py
vendored
Normal 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
7
vendor/symposion/conference/urls.py
vendored
Normal 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
16
vendor/symposion/conference/views.py
vendored
Normal 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
5
vendor/symposion/constants.py
vendored
Normal 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")
|
||||
|
BIN
vendor/symposion/locale/en/LC_MESSAGES/django.mo
vendored
Normal file
BIN
vendor/symposion/locale/en/LC_MESSAGES/django.mo
vendored
Normal file
Binary file not shown.
537
vendor/symposion/locale/en/LC_MESSAGES/django.po
vendored
Normal file
537
vendor/symposion/locale/en/LC_MESSAGES/django.po
vendored
Normal 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 ""
|
BIN
vendor/symposion/locale/ja/LC_MESSAGES/django.mo
vendored
Normal file
BIN
vendor/symposion/locale/ja/LC_MESSAGES/django.mo
vendored
Normal file
Binary file not shown.
573
vendor/symposion/locale/ja/LC_MESSAGES/django.po
vendored
Normal file
573
vendor/symposion/locale/ja/LC_MESSAGES/django.po
vendored
Normal 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
0
vendor/symposion/models.py
vendored
Normal file
1
vendor/symposion/proposals/__init__.py
vendored
Normal file
1
vendor/symposion/proposals/__init__.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = "symposion.proposals.apps.ProposalsConfig"
|
38
vendor/symposion/proposals/actions.py
vendored
Normal file
38
vendor/symposion/proposals/actions.py
vendored
Normal 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
32
vendor/symposion/proposals/admin.py
vendored
Normal 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
8
vendor/symposion/proposals/apps.py
vendored
Normal 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
45
vendor/symposion/proposals/forms.py
vendored
Normal 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",
|
||||
]
|
106
vendor/symposion/proposals/migrations/0001_initial.py
vendored
Normal file
106
vendor/symposion/proposals/migrations/0001_initial.py
vendored
Normal 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')]),
|
||||
),
|
||||
]
|
0
vendor/symposion/proposals/migrations/__init__.py
vendored
Normal file
0
vendor/symposion/proposals/migrations/__init__.py
vendored
Normal file
251
vendor/symposion/proposals/models.py
vendored
Normal file
251
vendor/symposion/proposals/models.py
vendored
Normal 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
|
0
vendor/symposion/proposals/templatetags/__init__.py
vendored
Normal file
0
vendor/symposion/proposals/templatetags/__init__.py
vendored
Normal file
72
vendor/symposion/proposals/templatetags/proposal_tags.py
vendored
Normal file
72
vendor/symposion/proposals/templatetags/proposal_tags.py
vendored
Normal 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
32
vendor/symposion/proposals/urls.py
vendored
Normal 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
414
vendor/symposion/proposals/views.py
vendored
Normal 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
1
vendor/symposion/reviews/__init__.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = "symposion.reviews.apps.ReviewsConfig"
|
20
vendor/symposion/reviews/admin.py
vendored
Normal file
20
vendor/symposion/reviews/admin.py
vendored
Normal 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
8
vendor/symposion/reviews/apps.py
vendored
Normal 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")
|
11
vendor/symposion/reviews/context_processors.py
vendored
Normal file
11
vendor/symposion/reviews/context_processors.py
vendored
Normal 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
49
vendor/symposion/reviews/forms.py
vendored
Normal 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.")
|
||||
)
|
0
vendor/symposion/reviews/management/__init__.py
vendored
Normal file
0
vendor/symposion/reviews/management/__init__.py
vendored
Normal file
0
vendor/symposion/reviews/management/commands/__init__.py
vendored
Normal file
0
vendor/symposion/reviews/management/commands/__init__.py
vendored
Normal file
12
vendor/symposion/reviews/management/commands/assign_reviewers.py
vendored
Normal file
12
vendor/symposion/reviews/management/commands/assign_reviewers.py
vendored
Normal 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)
|
9
vendor/symposion/reviews/management/commands/calculate_results.py
vendored
Normal file
9
vendor/symposion/reviews/management/commands/calculate_results.py
vendored
Normal 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()
|
25
vendor/symposion/reviews/management/commands/create_review_permissions.py
vendored
Normal file
25
vendor/symposion/reviews/management/commands/create_review_permissions.py
vendored
Normal 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)
|
16
vendor/symposion/reviews/management/commands/promoteproposals.py
vendored
Normal file
16
vendor/symposion/reviews/management/commands/promoteproposals.py
vendored
Normal 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))")
|
145
vendor/symposion/reviews/migrations/0001_initial.py
vendored
Normal file
145
vendor/symposion/reviews/migrations/0001_initial.py
vendored
Normal 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')]),
|
||||
),
|
||||
]
|
0
vendor/symposion/reviews/migrations/__init__.py
vendored
Normal file
0
vendor/symposion/reviews/migrations/__init__.py
vendored
Normal file
402
vendor/symposion/reviews/models.py
vendored
Normal file
402
vendor/symposion/reviews/models.py
vendored
Normal 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)
|
0
vendor/symposion/reviews/templatetags/__init__.py
vendored
Normal file
0
vendor/symposion/reviews/templatetags/__init__.py
vendored
Normal file
13
vendor/symposion/reviews/templatetags/review_tags.py
vendored
Normal file
13
vendor/symposion/reviews/templatetags/review_tags.py
vendored
Normal 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
44
vendor/symposion/reviews/urls.py
vendored
Normal 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
19
vendor/symposion/reviews/utils.py
vendored
Normal 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
658
vendor/symposion/reviews/views.py
vendored
Normal 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
1
vendor/symposion/schedule/__init__.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = "symposion.schedule.apps.ScheduleConfig"
|
57
vendor/symposion/schedule/admin.py
vendored
Normal file
57
vendor/symposion/schedule/admin.py
vendored
Normal 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
7
vendor/symposion/schedule/apps.py
vendored
Normal 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
163
vendor/symposion/schedule/forms.py
vendored
Normal 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
30
vendor/symposion/schedule/helpers.py
vendored
Normal 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))
|
187
vendor/symposion/schedule/migrations/0001_initial.py
vendored
Normal file
187
vendor/symposion/schedule/migrations/0001_initial.py
vendored
Normal 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')]),
|
||||
),
|
||||
]
|
18
vendor/symposion/schedule/migrations/0002_presentation_unpublish.py
vendored
Normal file
18
vendor/symposion/schedule/migrations/0002_presentation_unpublish.py
vendored
Normal 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'),
|
||||
),
|
||||
]
|
18
vendor/symposion/schedule/migrations/0003_auto_20161113_1530.py
vendored
Normal file
18
vendor/symposion/schedule/migrations/0003_auto_20161113_1530.py
vendored
Normal 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),
|
||||
),
|
||||
]
|
18
vendor/symposion/schedule/migrations/0003_slot_exclusive.py
vendored
Normal file
18
vendor/symposion/schedule/migrations/0003_slot_exclusive.py
vendored
Normal 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'),
|
||||
),
|
||||
]
|
14
vendor/symposion/schedule/migrations/0004_merge.py
vendored
Normal file
14
vendor/symposion/schedule/migrations/0004_merge.py
vendored
Normal 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 = [
|
||||
]
|
18
vendor/symposion/schedule/migrations/0005_auto_20161210_1736.py
vendored
Normal file
18
vendor/symposion/schedule/migrations/0005_auto_20161210_1736.py
vendored
Normal 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),
|
||||
),
|
||||
]
|
18
vendor/symposion/schedule/migrations/0006_room_track.py
vendored
Normal file
18
vendor/symposion/schedule/migrations/0006_room_track.py
vendored
Normal 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'),
|
||||
),
|
||||
]
|
39
vendor/symposion/schedule/migrations/0007_auto_20161224_1709.py
vendored
Normal file
39
vendor/symposion/schedule/migrations/0007_auto_20161224_1709.py
vendored
Normal 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')]),
|
||||
),
|
||||
]
|
0
vendor/symposion/schedule/migrations/__init__.py
vendored
Normal file
0
vendor/symposion/schedule/migrations/__init__.py
vendored
Normal file
307
vendor/symposion/schedule/models.py
vendored
Normal file
307
vendor/symposion/schedule/models.py
vendored
Normal 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])
|
0
vendor/symposion/schedule/tests/__init__.py
vendored
Normal file
0
vendor/symposion/schedule/tests/__init__.py
vendored
Normal file
13
vendor/symposion/schedule/tests/data/schedule.csv
vendored
Normal file
13
vendor/symposion/schedule/tests/data/schedule.csv
vendored
Normal 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"
|
|
14
vendor/symposion/schedule/tests/data/schedule_overlap.csv
vendored
Normal file
14
vendor/symposion/schedule/tests/data/schedule_overlap.csv
vendored
Normal 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"
|
|
65
vendor/symposion/schedule/tests/factories.py
vendored
Normal file
65
vendor/symposion/schedule/tests/factories.py
vendored
Normal 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
65
vendor/symposion/schedule/tests/runtests.py
vendored
Executable 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")
|
156
vendor/symposion/schedule/tests/test_forms.py
vendored
Normal file
156
vendor/symposion/schedule/tests/test_forms.py
vendored
Normal 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())
|
30
vendor/symposion/schedule/tests/test_views.py
vendored
Normal file
30
vendor/symposion/schedule/tests/test_views.py
vendored
Normal 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
|
58
vendor/symposion/schedule/tests/test_views_session.py
vendored
Normal file
58
vendor/symposion/schedule/tests/test_views_session.py
vendored
Normal 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
51
vendor/symposion/schedule/timetable.py
vendored
Normal 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
34
vendor/symposion/schedule/urls.py
vendored
Normal 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
399
vendor/symposion/schedule/views.py
vendored
Normal 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
1
vendor/symposion/speakers/__init__.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = "symposion.speakers.apps.SpeakersConfig"
|
8
vendor/symposion/speakers/admin.py
vendored
Normal file
8
vendor/symposion/speakers/admin.py
vendored
Normal 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
8
vendor/symposion/speakers/apps.py
vendored
Normal 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
32
vendor/symposion/speakers/forms.py
vendored
Normal 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
|
0
vendor/symposion/speakers/management/__init__.py
vendored
Normal file
0
vendor/symposion/speakers/management/__init__.py
vendored
Normal file
0
vendor/symposion/speakers/management/commands/__init__.py
vendored
Normal file
0
vendor/symposion/speakers/management/commands/__init__.py
vendored
Normal file
20
vendor/symposion/speakers/management/commands/export_speaker_data.py
vendored
Normal file
20
vendor/symposion/speakers/management/commands/export_speaker_data.py
vendored
Normal 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,
|
||||
])
|
48
vendor/symposion/speakers/migrations/0001_initial.py
vendored
Normal file
48
vendor/symposion/speakers/migrations/0001_initial.py
vendored
Normal 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',
|
||||
},
|
||||
),
|
||||
]
|
17
vendor/symposion/speakers/migrations/0002_auto_20161230_1900.py
vendored
Normal file
17
vendor/symposion/speakers/migrations/0002_auto_20161230_1900.py
vendored
Normal 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'},
|
||||
),
|
||||
]
|
0
vendor/symposion/speakers/migrations/__init__.py
vendored
Normal file
0
vendor/symposion/speakers/migrations/__init__.py
vendored
Normal file
132
vendor/symposion/speakers/models.py
vendored
Normal file
132
vendor/symposion/speakers/models.py
vendored
Normal 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
17
vendor/symposion/speakers/urls.py
vendored
Normal 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
133
vendor/symposion/speakers/views.py
vendored
Normal 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,
|
||||
})
|
1
vendor/symposion/sponsorship/__init__.py
vendored
Normal file
1
vendor/symposion/sponsorship/__init__.py
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
default_app_config = "symposion.sponsorship.apps.SponsorshipConfig"
|
128
vendor/symposion/sponsorship/admin.py
vendored
Normal file
128
vendor/symposion/sponsorship/admin.py
vendored
Normal 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
8
vendor/symposion/sponsorship/apps.py
vendored
Normal 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
91
vendor/symposion/sponsorship/forms.py
vendored
Normal 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"]
|
||||
)
|
0
vendor/symposion/sponsorship/management/__init__.py
vendored
Normal file
0
vendor/symposion/sponsorship/management/__init__.py
vendored
Normal file
0
vendor/symposion/sponsorship/management/commands/__init__.py
vendored
Normal file
0
vendor/symposion/sponsorship/management/commands/__init__.py
vendored
Normal file
77
vendor/symposion/sponsorship/management/commands/export_sponsors_data.py
vendored
Normal file
77
vendor/symposion/sponsorship/management/commands/export_sponsors_data.py
vendored
Normal 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"))
|
36
vendor/symposion/sponsorship/management/commands/reset_sponsor_benefits.py
vendored
Normal file
36
vendor/symposion/sponsorship/management/commands/reset_sponsor_benefits.py
vendored
Normal 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()
|
7
vendor/symposion/sponsorship/managers.py
vendored
Normal file
7
vendor/symposion/sponsorship/managers.py
vendored
Normal 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")
|
117
vendor/symposion/sponsorship/migrations/0001_initial.py
vendored
Normal file
117
vendor/symposion/sponsorship/migrations/0001_initial.py
vendored
Normal 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'),
|
||||
),
|
||||
]
|
0
vendor/symposion/sponsorship/migrations/__init__.py
vendored
Normal file
0
vendor/symposion/sponsorship/migrations/__init__.py
vendored
Normal file
331
vendor/symposion/sponsorship/models.py
vendored
Normal file
331
vendor/symposion/sponsorship/models.py
vendored
Normal 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)
|
0
vendor/symposion/sponsorship/templatetags/__init__.py
vendored
Normal file
0
vendor/symposion/sponsorship/templatetags/__init__.py
vendored
Normal file
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue