Changeset - 252697b842c0
[Not reviewed]
0 78 0
Joel Addison - 4 years ago 2020-11-22 13:58:14
joel@addison.net.au
Update to Django 2.2

Upgrade site and modules to Django 2.2. Remove and replace obsolete
functionality with current equivalents. Update requirements to latest
versions where possible. Remove unused dependencies.
78 files changed with 646 insertions and 364 deletions:
0 comments (0 inline, 0 general)
constraints.txt
Show inline comments
 
django<1.12,>=1.11
 
pysaml2==4.8.0
 
django<3.0,>=2.2
 
pysaml2>=5.3.0
docker/Dockerfile
Show inline comments
 
FROM python:3.6-stretch as symposion_base
 
FROM python:3.8-buster as symposion_base
 

	
 
RUN set -ex \
 
    && apt-get update
 
RUN set -ex \
 
    && buildDeps=' \
 
        libffi-dev \
 
        libfreetype6-dev \
 
        libjpeg-dev \
 
        libwebp-dev \
 
        libpng-dev \
 
        liblcms2-dev \
 
        zlib1g-dev \
 
        libmemcached-dev \
 
        libsasl2-dev \
 
        inkscape \
 
    ' \
 
    && apt-get install -y git xmlsec1 \
 
    && apt-get install -y $buildDeps --no-install-recommends \
 
    && rm -rf /var/lib/apt/lists/*
 

	
 
RUN set -ex \
 
    && pip install uwsgi
 

	
 
COPY constraints.txt requirements.txt /reqs/
 

	
 
RUN set -ex \
 
    && pip install -U pip \
 
    && pip install --no-cache-dir -r /reqs/requirements.txt -c /reqs/constraints.txt \
 
    && apt-get purge -y --auto-remove $buildDeps \
 
    && rm -rf /usr/src/python ~/.cache
 

	
 
COPY . /app/symposion_app
 

	
 
WORKDIR /app/symposion_app
 
RUN set -x \
 
    && pip install -r vendored_requirements.txt -c /reqs/constraints.txt
 
RUN set -x \
 
    && DJANGO_SECRET_KEY=1234 STRIPE_PUBLIC_KEY=1234 STRIPE_SECRET_KEY=1234 \
 
       DATABASE_URL="sqlite:////dev/null" python manage.py compilescss
 
RUN set -x \
 
    && DJANGO_SECRET_KEY=1234 STRIPE_PUBLIC_KEY=1234 STRIPE_SECRET_KEY=1234 \
 
       DATABASE_URL="sqlite:////dev/null" \
 
       python manage.py collectstatic --noinput -l -v 0
 
RUN set -ex \
 
    && cp static/build/fonts/*.ttf /usr/local/share/fonts/ \
 
    && fc-cache \
 
    && fc-list
 

	
 
FROM symposion_base as symposion_dev
 
VOLUME /app/symposion_app
 
CMD ["./manage.py", "runserver", "-v3", "0.0.0.0:8000"]
 

	
 
FROM symposion_base as symposion_prod
 
CMD ["/usr/local/bin/uwsgi", "--http-socket", "0.0.0.0:8000", "-b", "8192", "--wsgi-file", "pinaxcon/wsgi.py"]
docker/laptop-mode-env
Show inline comments
 
DJANGO_SECRET_KEY=5CEA51A5-A613-4AEF-A9FB-D0A57D77C13B
 
STRIPE_PUBLIC_KEY=5CEA51A5-A613-4AEF-A9FB-D0A57D77C13B
 
STRIPE_SECRET_KEY=5CEA51A5-A613-4AEF-A9FB-D0A57D77C13B
 
GCS_BUCKET=5CEA51A5-A613-4AEF-A9FB-D0A57D77C13B
 
GOOGLE_APPLICATION_CREDENTIALS=/dev/null
 
DATABASE_URL=sqlite:////tmp/symposion.sqlite
 
SYMPOSION_DEV_MODE=LAPTOP
 
SYMPOSION_APP_DEBUG=1
...
 
\ No newline at end of file
 
SYMPOSION_APP_DEBUG=1
pinaxcon/devmode_settings.py
Show inline comments
 
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
 
AUTHENTICATION_BACKENDS = [
 
    'symposion.teams.backends.TeamPermissionsBackend',
 
    'django.contrib.auth.backends.ModelBackend',
 
]
 
LOGIN_URL='/accounts/login'
 

	
 
ROOT_URLCONF = "pinaxcon.devmode_urls"
 

	
 
DEFAULT_FILE_STORAGE = 'django.core.files.storage.FileSystemStorage'
 

	
 
INTERNAL_IPS = ['*']
pinaxcon/devmode_urls.py
Show inline comments
 
from django.conf.urls import include, url
 
from django.contrib.auth.views import login, logout
 
    
 
from django.contrib.auth.views import LoginView, LogoutView
 
from django.urls import include, path
 

	
 
from pinaxcon import urls
 

	
 
urlpatterns = [
 
    url(r'^accounts/logout', logout, {'template_name': 'admin/logout.html'}),
 
    url(r'^accounts/login', login, {'template_name': 'admin/login.html'}),
 
    path('accounts/logout', LogoutView.as_view(template_name='admin/logout.html')),
 
    path('accounts/login', LoginView.as_view(template_name='admin/login.html')),
 
]
 

	
 
urlpatterns += urls.urlpatterns
pinaxcon/monkey_patch.py
Show inline comments
 
from functools import wraps
 

	
 

	
 
class MonkeyPatchMiddleware(object):
 
    ''' Ensures that our monkey patching only gets called after it is safe to do so.'''
 

	
 
    def process_request(self, request):
 
    def __init__(self, get_response):
 
        self.get_response = get_response
 

	
 
    def __call__(self, request):
 
        do_monkey_patch()
 
        response = self.get_response(request)
 
        return response
 

	
 

	
 
def do_monkey_patch():
 
    patch_stripe_payment_form()
 

	
 
    # Remove this function from existence
 
    global do_monkey_patch
 
    do_monkey_patch = lambda: None  # noqa: E731
 

	
 

	
 
def patch_stripe_payment_form():  # noqa: C901
 

	
 
    import inspect  # Oh no.
 
    from django.http.request import HttpRequest
 
    from registripe.forms import CreditCardForm
 
    from pinaxcon.registrasion import models
 

	
 
    old_init = CreditCardForm.__init__
 

	
 
    @wraps(old_init)
 
    def new_init(self, *a, **k):
 

	
 
        # Map the names from our attendee profile model
 
        # To the values expected in the Stripe card model
 
        mappings = (
 
            ("address_line_1", "address_line1"),
 
            ("address_line_2", "address_line2"),
 
            ("address_suburb", "address_city"),
 
            ("address_postcode", "address_zip"),
 
            ("state", "address_state"),
 
            ("country", "address_country"),
 
        )
 

	
 
        initial = "initial"
 
        if initial not in k:
 
            k[initial] = {}
 
        initial = k[initial]
 

	
 
        # Find request context maybe?
 
        frame = inspect.currentframe()
 
        attendee_profile = None
 
        if frame:
 
            context = frame.f_back.f_locals
 
            for name, value in (context.items() or {}):
 
                if not isinstance(value, HttpRequest):
 
                    continue
 
                user = value.user
 
                if not user.is_authenticated():
 
                if not user.is_authenticated:
 
                    break
 
                try:
 
                    attendee_profile = models.AttendeeProfile.objects.get(
 
                        attendee__user=user
 
                    )
 
                except models.AttendeeProfile.DoesNotExist:
 
                    # Profile is still none.
 
                    pass
 
                break
 

	
 
        if attendee_profile:
 
            for us, stripe in mappings:
 
                i = getattr(attendee_profile, us, None)
 
                if i:
 
                    initial[stripe] = i
 

	
 
        old_init(self, *a, **k)
 

	
 
    CreditCardForm.__init__ = new_init
pinaxcon/raffle/models.py
Show inline comments
 
from django.conf import settings
 
from django.db import models
 

	
 
from pinaxcon.raffle.mixins import PrizeMixin, RaffleMixin
 

	
 

	
 
class Raffle(RaffleMixin, models.Model):
 
    """
 
    Stores a single Raffle object, related to one or many
 
    :model:`pinaxcon_registrasion.Product`, which  is usually a raffle ticket,
 
    but can be set to tickets or other products for door prizes.
 
    """
 
    description = models.CharField(max_length=255)
 
    products = models.ManyToManyField('registrasion.Product')
 
    hidden = models.BooleanField(default=True)
 

	
 
    def __str__(self):
 
        return self.description
 

	
 

	
 
class Prize(PrizeMixin, models.Model):
 
    """
 
    Stores a Prize for a given :model:`pinaxcon_raffle.Raffle`.
 

	
 
    Once `winning_ticket` has been set to a :model:`pinaxcon_raffle.DrawnTicket`
 
    object, no further changes are permitted unless the object is explicitely
 
    unlocked.
 
    """
 
    description = models.CharField(max_length=255)
 
    raffle = models.ForeignKey('pinaxcon_raffle.Raffle', related_name='prizes')
 
    raffle = models.ForeignKey(
 
        'pinaxcon_raffle.Raffle',
 
        related_name='prizes',
 
        on_delete=models.CASCADE,
 
    )
 
    order = models.PositiveIntegerField()
 
    winning_ticket = models.OneToOneField(
 
        'pinaxcon_raffle.DrawnTicket', null=True,
 
        blank=True, related_name='+', on_delete=models.PROTECT
 
        'pinaxcon_raffle.DrawnTicket',
 
        null=True,
 
        blank=True,
 
        related_name='+',
 
        on_delete=models.PROTECT,
 
    )
 

	
 
    class Meta:
 
        unique_together = ('raffle', 'order')
 

	
 
    def __str__(self):
 
        return f"{self.order}. Prize: {self.description}"
 

	
 

	
 
class PrizeAudit(models.Model):
 
    """
 
    Stores an audit event for changes to a particular :model:`pinaxcon_raffle.Prize`.
 
    """
 
    reason = models.CharField(max_length=255)
 
    prize = models.ForeignKey('pinaxcon_raffle.Prize', related_name='audit_events')
 
    prize = models.ForeignKey(
 
        'pinaxcon_raffle.Prize',
 
        related_name='audit_events',
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    user = models.ForeignKey('auth.User')
 
    user = models.ForeignKey(
 
        settings.AUTH_USER_MODEL,
 
        on_delete=models.CASCADE,
 
    )
 
    timestamp = models.DateTimeField(auto_now_add=True)
 

	
 
    class Meta:
 
        ordering = ('-timestamp',)
 

	
 
    def __str__(self):
 
        return self.reason
 

	
 

	
 
class Draw(models.Model):
 
    """
 
    Stores a draw for a given :model:`pinaxcon_raffle.Raffle`, along with audit fields
 
    for the creating :model:`auth.User` and the creation timestamp.
 
    """
 
    raffle = models.ForeignKey('pinaxcon_raffle.Raffle', related_name='draws')
 
    drawn_by = models.ForeignKey('auth.User')
 
    raffle = models.ForeignKey(
 
        'pinaxcon_raffle.Raffle',
 
        related_name='draws',
 
        on_delete=models.CASCADE,
 
    )
 
    drawn_by = models.ForeignKey(
 
        settings.AUTH_USER_MODEL,
 
        on_delete=models.CASCADE,
 
    )
 
    drawn_time = models.DateTimeField(auto_now_add=True)
 

	
 
    def __str__(self):
 
        return f"{self.raffle}: {self.drawn_time}"
 

	
 

	
 
class DrawnTicket(models.Model):
 
    """
 
    Stores the result of a ticket draw, along with the corresponding
 
    :model:`pinaxcon_raffle.Draw`, :model:`pinaxcon_raffle.Prize` and the
 
    :model:`registrasion.commerce.LineItem` from which it was generated.
 
    """
 
    ticket = models.CharField(max_length=255)
 

	
 
    draw = models.ForeignKey('pinaxcon_raffle.Draw')
 
    prize = models.ForeignKey('pinaxcon_raffle.Prize')
 
    lineitem = models.ForeignKey('registrasion.LineItem')
 
    draw = models.ForeignKey(
 
        'pinaxcon_raffle.Draw',
 
        on_delete=models.CASCADE,
 
    )
 
    prize = models.ForeignKey(
 
        'pinaxcon_raffle.Prize',
 
        on_delete=models.CASCADE,
 
    )
 
    lineitem = models.ForeignKey(
 
        'registrasion.LineItem',
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    def __str__(self):
 
        return f"{self.ticket}: {self.draw.raffle}"
...
 
\ No newline at end of file
 
        return f"{self.ticket}: {self.draw.raffle}"
pinaxcon/registrasion/management/commands/dummy_presentations.py
Show inline comments
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from django.core.management.base import BaseCommand
 

	
 
from symposion.conference.models import Section, current_conference
 

	
 
from symposion.speakers.models import Speaker
 
from symposion.schedule.models import Presentation
 
from symposion.proposals.models import ProposalKind
 
from pinaxcon.proposals.models import TalkProposal
 

	
 
User = get_user_model()
 

	
 

	
 
class Command(BaseCommand):
 

	
 
    help = "Creates a bunch of dummy presentations to play around with."
 

	
 
    def handle(self, *args, **options):
 
        conf = current_conference()
 
        section = Section.objects.filter(conference=conf, slug="main").all().first()
 

	
 
        user = User.objects.first()
 
        speaker = Speaker.objects.first()
 
        if not speaker:
 
            speaker, _ = Speaker.objects.get_or_create(name="Dummy Speaker",
 
                                                       defaults={"user": user})
 
        talk_kind = ProposalKind.objects.first()
 
        target_audience = TalkProposal.TARGET_USER
 

	
 
        for i in range(1000, 1020):
 
            prop, _created = TalkProposal.objects.get_or_create(
 
                pk=i, kind=talk_kind, speaker=speaker, target_audience=target_audience,
 
                title=f"dummy title {i}", abstract=f"dummy abstract {i}")
 

	
 
            pres, _created = Presentation.objects.get_or_create(
 
                proposal_base=prop, section=section, speaker=speaker,
 
                title=f"dummy title {i}", abstract=f"dummy abstract {i}")
pinaxcon/settings.py
Show inline comments
 
from decimal import Decimal
 
import os
 
import sys
 

	
 
import django
 
import dj_database_url
 
import saml2
 
import saml2.saml
 

	
 
from datetime import date, datetime, timedelta
 
import pytz
 

	
 
from dataclasses import dataclass
 

	
 
PROJECT_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
 
PACKAGE_ROOT = os.path.abspath(os.path.dirname(__file__))
 
DJANGO_ROOT = os.path.abspath(os.path.dirname(django.__file__))
 
BASE_DIR = PACKAGE_ROOT
 
sys.path.append(os.path.join(PROJECT_ROOT, 'vendor'))
 

	
 

	
 
### USER SETTINGS
 
DEV_MODE = os.environ.get("SYMPOSION_DEV_MODE", None)
 
DEBUG = os.environ.get('SYMPOSION_APP_DEBUG', '0')
 
if isinstance(DEBUG, str):
 
    try:
 
        i = int(DEBUG)
 
        if not i in [0, 1]:
 
            raise ValueError("not 0 or 1")
 
        DEBUG = bool(i)
 
    except ValueError:
 
        sys.exit('DEBUG env var must be set to string value of a 0 or 1')
 
else:
 
    sys.exit('DEBUG env var is in unexpected format.  Should be a string'
 
             'containing either a 0 or a 1 - Got type %s' % type(DEBUG))
 

	
 
DATABASES = {}
 
DATABASES['default'] = dj_database_url.config(conn_max_age=600)
 
if DATABASES['default']['ENGINE'] == 'django.db.backends.mysql':
 
    DATABASES['default']['OPTIONS'] = {'charset': 'utf8mb4'}
 

	
 
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
 
EMAIL_HOST = os.environ.get('EMAIL_HOST', None)
 
EMAIL_PORT = os.environ.get('EMAIL_PORT', 25)
 
EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER', None)
 
EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD', None)
 
DEFAULT_FROM_EMAIL = os.environ.get('DEFAULT_FROM_EMAIL', 'webmaster@localhost')
 
EMAIL_USE_SSL = False
 
EMAIL_USE_TLS = False
 
_EMAIL_SSL_FLAVOR=os.environ.get('EMAIL_SSL_FLAVOR', None)
 
if _EMAIL_SSL_FLAVOR == "TLS":
 
    EMAIL_USE_TLS = True
 
elif _EMAIL_SSL_FLAVOR == "SSL":
 
    EMAIL_USE_SSL = True
 

	
 
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', None)
 

	
 
PINAX_STRIPE_PUBLIC_KEY = os.environ.get('STRIPE_PUBLIC_KEY', None)
 
PINAX_STRIPE_SECRET_KEY = os.environ.get('STRIPE_SECRET_KEY', None)
 
PINAX_STRIPE_SEND_EMAIL_RECEIPTS = False
 

	
 
ANALYTICS_KEY = os.environ.get('ANALYTICS_KEY', None)
 

	
 
saml2_entityid = os.environ.get('SAML2_ENTITYID', None)
 
saml2_sp_name = os.environ.get('SAML2_SP_NAME', None)
 
saml2_sp_assertion_service = os.environ.get('SAML2_SP_ASSERTION_SERVICE', None)
 
saml2_sp_slo_rdir = os.environ.get('SAML2_SP_SLO_RDIR', None)
 
saml2_sp_slo_post = os.environ.get('SAML2_SP_SLO_POST', None)
 

	
 
saml2_idp_metadata = {
 
    'local': [os.environ.get('SAML2_IDP_METADATA_FILE', None)],
 
    }
 
saml2_signing_key = os.environ.get('SAML2_SIGNING_KEY', None)
 
saml2_signing_crt = os.environ.get('SAML2_SIGNING_CRT', None)
 
saml2_encr_key = os.environ.get('SAML2_ENCRYPTION_KEY', None)
 
saml2_encr_crt = os.environ.get('SAML2_ENCRYPTION_CRT', None)
 
saml2_contact = {
 
    'given_name': os.environ.get("META_GIVEN_NAME", 'Bastard'),
 
    'sur_name': os.environ.get('META_FAM_NAME', 'Operator'),
 
    'company': os.environ.get('META_COMPANY', 'Corp1'),
 
    'email_address': os.environ.get('META_EMAIL', 'op@example.com'),
 
    'contact_type': 'technical'},
 

	
 
fail = False
 

	
 
BADGER_DEFAULT_SVG = 'registrasion/badge.svg'
 
BADGER_DEFAULT_FORM = "registrasion/badge_form.html"
 

	
 
if SECRET_KEY is None:
 
    print("FAILURE: You need to supply a DJANGO_SECRET_KEY "
 
          "environment variable")
 
    fail = True
 

	
 
if PINAX_STRIPE_PUBLIC_KEY is None:
 
    print("FAILURE: You need to supply a STRIPE_PUBLIC_KEY "
 
          "environment variable")
 
    fail = True
 

	
 
if PINAX_STRIPE_SECRET_KEY is None:
 
    print("FAILURE: You need to supply a STRIPE_SECRET_KEY "
 
          "environment variable")
 
    fail = True
 

	
 
if fail:
 
    sys.exit('FAILURE: Missing environment variables.')
 

	
 
### Standard settings
 

	
 
ADMIN_USERNAMES = []
 

	
 
if DEV_MODE and DEV_MODE == "LAPTOP":
 
    CACHES = {
 
        'default': {
 
            'BACKEND': 'django.core.cache.backends.dummy.DummyCache',
 
        }
 
    }
 
else:
 
    CACHES = {
 
        'default': {
 
            'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
 
            'LOCATION': 'unique-snowflake',
 
        }
 
    }
 

	
 

	
 
ALLOWED_HOSTS = ['127.0.0.1', 'localhost', '*']
 

	
 
TIME_ZONE = "Australia/Brisbane"
 
TIME_ZONE = "Australia/Melbourne"
 
DATE_FORMAT = "j F Y"
 
LANGUAGE_CODE = "en-au"
 

	
 
SITE_ID = int(os.environ.get("SITE_ID", 1))
 
USE_I18N = True
 
USE_L10N = True
 
USE_TZ = True
 

	
 
MEDIA_ROOT = os.environ.get("MEDIA_ROOT", os.path.join(PACKAGE_ROOT, "site_media", "media"))
 
MEDIA_URL = "/site_media/media/"
 

	
 
STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static', 'build')
 
STATIC_URL = '/static/build/'
 

	
 
STATICFILES_DIRS = [
 
    os.path.join(PROJECT_ROOT, 'static', 'src'),
 
]
 

	
 
STATICFILES_FINDERS = [
 
    "django.contrib.staticfiles.finders.FileSystemFinder",
 
    "django.contrib.staticfiles.finders.AppDirectoriesFinder",
 
    "sass_processor.finders.CssFinder",
 
]
 

	
 
TEMPLATES = [
 
    {
 
        "BACKEND": "django.template.backends.django.DjangoTemplates",
 
        "DIRS": [
 
            os.path.join(PACKAGE_ROOT, "templates"),
 
            os.path.join(DJANGO_ROOT, 'forms/templates')
 
        ],
 
        "APP_DIRS": True,
 
        "OPTIONS": {
 
            "debug": DEBUG,
 
            "context_processors": [
 
                "django.contrib.auth.context_processors.auth",
 
                "django.template.context_processors.debug",
 
                "django.template.context_processors.i18n",
 
                "django.template.context_processors.media",
 
                "django.template.context_processors.static",
 
                "django.template.context_processors.tz",
 
                "django.template.context_processors.request",
 
                "django.contrib.messages.context_processors.messages",
 
                "pinax_theme_bootstrap.context_processors.theme",
 
                "symposion.reviews.context_processors.reviews",
 
                "django_settings_export.settings_export",
 
            ],
 
        },
 
    },
 
]
 

	
 
MIDDLEWARE_CLASSES = [
 
MIDDLEWARE = [
 
    "whitenoise.middleware.WhiteNoiseMiddleware",
 
    "django.contrib.sessions.middleware.SessionMiddleware",
 
    "django.middleware.common.CommonMiddleware",
 
    "django.middleware.csrf.CsrfViewMiddleware",
 
    "django.contrib.auth.middleware.AuthenticationMiddleware",
 
    "django.contrib.auth.middleware.SessionAuthenticationMiddleware",
 
    "django.contrib.messages.middleware.MessageMiddleware",
 
    "debug_toolbar.middleware.DebugToolbarMiddleware",
 
    "reversion.middleware.RevisionMiddleware",
 
    "waffle.middleware.WaffleMiddleware",
 
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
 
    "django.contrib.flatpages.middleware.FlatpageFallbackMiddleware",
 
    'pinaxcon.monkey_patch.MonkeyPatchMiddleware',
 
]
 

	
 
if DEV_MODE and DEV_MODE == "LAPTOP":
 
    ROOT_URLCONF = "pinaxcon.devmode_urls"
 
else:
 
    ROOT_URLCONF = "pinaxcon.urls"
 

	
 
# Python dotted path to the WSGI application used by Django's runserver.
 
WSGI_APPLICATION = "pinaxcon.wsgi.application"
 

	
 
INSTALLED_APPS = [
 
    "whitenoise.runserver_nostatic",
 
    "django.contrib.admin",
 
    "django.contrib.auth",
 
    "django.contrib.contenttypes",
 
    "django.contrib.flatpages",
 
    "django.contrib.messages",
 
    "django.contrib.sessions",
 
    "django.contrib.sites",
 
    "django.contrib.staticfiles",
 
    "django.contrib.humanize",
 
    "debug_toolbar",
 

	
 
    'djangosaml2',
 

	
 
    # theme
 
    "bootstrapform",
 
    "pinax_theme_bootstrap",
 
    "sass_processor",
 
    "capture_tag",
 

	
 
    # external
 
    "easy_thumbnails",
 
    "taggit",
 
    "reversion",
 
    "sitetree",
 
    "django_jsonfield_backport",
 
    "pinax.eventlog",
 

	
 
    # symposion
 
    "symposion",
 
    "symposion.conference",
 
    "symposion.proposals",
 
    "symposion.reviews",
 
    "symposion.schedule",
 
    "symposion.speakers",
 
    "symposion.teams",
 

	
 
    # Registrasion
 
    "registrasion",
 

	
 
    # Registrasion-stripe
 
    "pinax.stripe",
 
    "django_countries",
 
    "registripe",
 

	
 
    #registrasion-desk
 
    "regidesk",
 

	
 
    # admin - required by registrasion ??
 
    "nested_admin",
 

	
 
    # project
 
    "pinaxcon",
 
    "pinaxcon.proposals",
 
    "pinaxcon.registrasion",
 
    "pinaxcon.raffle",
 
    "jquery",
 
    "djangoformsetjs",
 

	
 
    # testing and rollout
 
    "django_nose",
 
    "waffle",
 

	
 
    "crispy_forms",
 
]
 

	
 
CRISPY_TEMPLATE_PACK = "bootstrap4"
 

	
 
DEBUG_TOOLBAR_PANELS = [
 
    'debug_toolbar.panels.versions.VersionsPanel',
 
    'debug_toolbar.panels.timer.TimerPanel',
 
    'debug_toolbar.panels.settings.SettingsPanel',
 
    'debug_toolbar.panels.headers.HeadersPanel',
 
    'debug_toolbar.panels.request.RequestPanel',
 
    'debug_toolbar.panels.sql.SQLPanel',
 
    'debug_toolbar.panels.staticfiles.StaticFilesPanel',
 
    'debug_toolbar.panels.cache.CachePanel',
 
    'debug_toolbar.panels.signals.SignalsPanel',
 
    'debug_toolbar.panels.logging.LoggingPanel',
 
    'debug_toolbar.panels.templates.TemplatesPanel',
 
    'debug_toolbar.panels.redirects.RedirectsPanel',
 
]
 

	
 
DEBUG_TOOLBAR_CONFIG = {
 
    'INTERCEPT_REDIRECTS': False,
 
    'SHOW_TOOLBAR_CALLBACK': lambda x: DEBUG,
 
}
 

	
 
INTERNAL_IPS = [
 
    '127.0.0.1',
 
]
 

	
 
from debug_toolbar.panels.logging import collector
 
LOGGING = {
 
    'version': 1,
 
    'disable_existing_loggers': False,
 
    'formatters': {
 
        'verbose': {
 
            'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
 
        },
 
        'simple': {
 
            'format': '%(asctime)s %(levelname)s $(module)s %(message)s'
 
        },
 
    },
 
    'filters': {
 
        'require_debug_false': {
 
            '()': 'django.utils.log.RequireDebugFalse'
 
        }
 
    },
 
    'handlers': {
 
        'console': {
 
            'level': 'DEBUG',
 
            'class': 'logging.StreamHandler',
 
            'formatter': 'simple'
 
        },
 
        'mail_admins': {
 
            'level': 'ERROR',
 
            'filters': ['require_debug_false'],
 
            'class': 'django.utils.log.AdminEmailHandler',
 
            'include_html': True,
 
        },
 
        'djdt_log': {
 
            'level': 'DEBUG',
 
            'class': 'debug_toolbar.panels.logging.ThreadTrackingHandler',
 
            'collector': collector,
 
        },
 
    },
 
    'loggers': {
 
        'django.request': {
 
            'handlers': ['mail_admins'],
 
            'level': 'DEBUG',
 
            'propagate': True,
 
        },
 
        'symposion.request': {
 
            'handlers': ['mail_admins'],
 
            'level': 'DEBUG',
 
            'propagate': True,
 
        },
 
    },
 
    'root': {
 
        'handlers': ['console', 'djdt_log'],
 
        'level': 'DEBUG'
 
    },
 
}
 
FIXTURE_DIRS = [
 
    os.path.join(PROJECT_ROOT, "fixtures"),
 
]
 

	
 
AUTHENTICATION_BACKENDS = [
 
    'symposion.teams.backends.TeamPermissionsBackend',
 
    'django.contrib.auth.backends.ModelBackend',
 
    'djangosaml2.backends.Saml2Backend',
 
]
 

	
 
LOGIN_URL = '/saml2/login/'
 
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
 

	
 
CONFERENCE_ID = 1
 
PROPOSAL_FORMS = {
 
    "talk": "pinaxcon.proposals.forms.TalkProposalForm",
 
    "tutorial": "pinaxcon.proposals.forms.TutorialProposalForm",
 
    "miniconf": "pinaxcon.proposals.forms.MiniconfProposalForm",
 
}
 
MAIN_CONFERENCE_PROPOSAL_KINDS = ("Talk", "Miniconf")
 

	
 
# Registrasion bits:
 
ATTENDEE_PROFILE_MODEL = "pinaxcon.registrasion.models.AttendeeProfile"
 
ATTENDEE_PROFILE_FORM = "pinaxcon.registrasion.forms.ProfileForm"
 
INVOICE_CURRENCY = "AUD"
 
GST_RATE =  Decimal('0.1')
 
TICKET_PRODUCT_CATEGORY = 1
 
TERMS_PRODUCT_CATEGORY = 2
 
ATTENDEE_PROFILE_FORM = "pinaxcon.registrasion.forms.ProfileForm"
 

	
 
#REGIDESK
 
REGIDESK_BOARDING_GROUP = "Ready For Boarding"
 

	
 
# CSRF custom error screen
 
CSRF_FAILURE_VIEW = "pinaxcon.csrf_view.csrf_failure"
 

	
 
# Use nose to run all tests
 
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
 

	
 
# Tell nose to measure coverage on the 'foo' and 'bar' apps
 
NOSE_ARGS = [
 
    '--with-coverage',
 
    '--cover-package=registrasion.controllers,registrasion.models',
 
]
...
 
@@ -409,154 +415,155 @@ SAML_CONFIG = {
 
                'assertion_consumer_service': [
 
                    saml2_sp_assertion_service,
 
                    ],
 
                'single_logout_service': [
 
                    (saml2_sp_slo_rdir, saml2.BINDING_HTTP_REDIRECT),
 
                    (saml2_sp_slo_post, saml2.BINDING_HTTP_POST),
 
                    ],
 
                },
 
            'logout_requests_signed': True,
 
            'required_attributes': ['uid', 'mail', 'givenName', 'sn'],
 
            },
 
        },
 
    'metadata': saml2_idp_metadata,
 
    'debug': 0,
 
    'key_file': saml2_signing_key,
 
    'cert_file': saml2_signing_crt,
 
    'encryption_keypairs': [{
 
        'key_file': saml2_encr_key,
 
        'cert_file': saml2_encr_crt,
 
    }],
 
    'contact_person': saml2_contact,
 
    'valid_for': 10,
 
}
 

	
 
if 'SAML_CONFIG_LOADER' in os.environ:
 
    SAML_CONFIG_LOADER = os.environ.get('SAML_CONFIG_LOADER')
 

	
 
DEFAULT_FILE_STORAGE = os.environ.get('DEFAULT_FILE_STORAGE', 'gapc_storage.storage.GoogleCloudStorage')
 
GAPC_STORAGE = {
 
    'num_retries': 2,
 
}
 

	
 
SETTINGS_EXPORT = [
 
    'DEBUG',
 
    'ANALYTICS_KEY',
 
]
 

	
 
if DEV_MODE and DEV_MODE == "LAPTOP":
 
    print("ENABLING LAPTOP MODE")
 
    from .devmode_settings import *
 

	
 

	
 
class Category(object):
 
    tickets = []
 

	
 
    @classmethod
 
    def order(cls, ticket) -> int:
 
        return (cls.tickets.index(ticket) + 1) * 10
 

	
 

	
 
@dataclass(frozen=True)
 
class Ticket:
 
    name: str
 
    regular_price: Decimal
 
    earlybird_price: Decimal
 

	
 
    def earlybird_discount(self):
 
        return self.regular_price - self.earlybird_price
 

	
 

	
 
@dataclass(frozen=True)
 
class DinnerTicket:
 
    name: str
 
    price: Decimal
 
    description: str
 
    reservation: timedelta
 
    cat: Category
 

	
 
    def order(self):
 
        return self.cat.order(self)
 

	
 

	
 
class PenguinDinnerTicket(DinnerTicket):
 
    pass
 

	
 

	
 
class SpeakersDinnerTicket(DinnerTicket):
 
    pass
 

	
 

	
 
class SpeakersDinnerCat(Category):
 
    @classmethod
 
    def create(cls, name: str, price: Decimal, description: str, reservation: timedelta) -> SpeakersDinnerTicket:
 
        t = SpeakersDinnerTicket(name, price, description, reservation, cls)
 
        cls.tickets.append(t)
 
        return t
 

	
 

	
 
class PenguinDinnerCat(Category):
 
    @classmethod
 
    def create(cls, name: str, price: Decimal, description: str, reservation: timedelta) -> PenguinDinnerTicket:
 
        t = PenguinDinnerTicket(name, price, description, reservation, cls)
 
        cls.tickets.append(t)
 
        return t
 

	
 

	
 
LCA_START = datetime(2021, 1, 23)
 
LCA_END = datetime(2021, 1, 25)
 
EARLY_BIRD_DEADLINE = datetime(2020, 12, 1)
 
_TZINFO = pytz.timezone(TIME_ZONE)
 
LCA_START = datetime(2021, 1, 23, tzinfo=_TZINFO)
 
LCA_END = datetime(2021, 1, 25, tzinfo=_TZINFO)
 
EARLY_BIRD_DEADLINE = datetime(2020, 12, 1, tzinfo=_TZINFO)
 
PENGUIN_DINNER_TICKET_DATE = date(2021, 1, 23)
 
SPEAKER_DINNER_TICKET_DATE = date(2021, 1, 25)
 
PDNS_TICKET_DATE = date(2021, 1, 24)
 

	
 
TSHIRT_PRICE = Decimal("25.00")
 

	
 
CONTRIBUTOR = Ticket("Contributor", Decimal("1999.00"), Decimal("1849.00"))
 
PROFESSIONAL = Ticket("Professional", Decimal("1099.00"), Decimal("949.00"))
 
HOBBYIST = Ticket("Hobbyist", Decimal("549.00"), Decimal("399.00"))
 
STUDENT = Ticket("Student", Decimal("199.00"), None)
 

	
 
MINICONF_MT = Ticket("Monday and Tuesday Only", Decimal("198.00"), None)
 
MINICONF_M = Ticket("Monday Only", Decimal("99.00"), None)
 
MINICONF_T = Ticket("Tuesday Only", Decimal("99.00"), None)
 

	
 
MEDIA = Ticket("Media", Decimal("0.0"), None)
 
SPEAKER = Ticket("Speaker", Decimal("0.0"), None)
 
SPONSOR = Ticket("Sponsor", Decimal("0.0"), None)
 

	
 
CONFERENCE_ORG = Ticket("Conference Organiser", Decimal("0.0"), None)
 
CONFERENCE_VOL = Ticket("Conference Volunteer", Decimal("0.0"), None)
 

	
 
PENGUIN_DINNER = PenguinDinnerCat
 
PENGUIN_DINNER_ADULT = PenguinDinnerCat.create(
 
    "Adult", Decimal("95.00"),
 
    "Includes an adult's meal and full beverage service.",
 
    timedelta(hours=1))
 
PENGUIN_DINNER_CHILD = PenguinDinnerCat.create(
 
    "Child", Decimal("50.00"),
 
    "Children 14 and under. "
 
    "Includes a child's meal and soft drink service.",
 
    timedelta(hours=1))
 
PENGUIN_DINNER_INFANT = PenguinDinnerCat.create(
 
    "Infant", Decimal("0.0"),
 
    "Includes no food or beverage service.",
 
    timedelta(hours=1))
 

	
 
SPEAKERS_DINNER = SpeakersDinnerCat
 

	
 
SPEAKERS_DINNER_ADULT = SpeakersDinnerCat.create(
 
    "Adult", Decimal("100.00"),
 
    "Includes an adult's meal and full beverage service.",
 
    timedelta(hours=1))
 

	
 
# SPEAKERS_DINNER_CHILD = SpeakersDinnerCat.create(
 
#     "Child", Decimal("60.00"),
 
#     "Children 14 and under. "
 
#     "Includes a child's meal and soft drink service.",
 
#     timedelta(hours=1))
 

	
 
# SPEAKERS_DINNER_INFANT = SpeakersDinnerCat.create(
 
#     "Infant", Decimal("00.00"),
 
#     "Infant must be seated in an adult's lap. "
 
#     "No food or beverage service.",
 
#     timedelta(hours=1))
pinaxcon/templates/403_csrf.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% load i18n %}
 

	
 
{% block body_class %}template-blogpage{% endblock %}
 

	
 
{% block head_title %}{{ page.title }}{% endblock %}
 

	
 
{% block body %}
 
  {% block content %}
 
    <div class="l-content-page">
 
      <div class="l-content-page--richtext">
 
        <h2>{{ title }} <span>(403)</span></h2>
 

	
 
        <p>{{ main }}</p>
 

	
 
        {% if bad_token and request.user.is_authenticated %}
 
          <p>You are already logged in. If you saw this issue whilst attempting
 
            to log in, you can to go to the
 
            <a href='{% url "dashboard" %}'>Dashboard</a> and continue using
 
            the site.</p>
 
        {% endif %}
 

	
 
        {% if no_referer %}
 
          <p>{{ no_referer1 }}</p>
 
          <p>{{ no_referer2 }}</p>
 
        {% endif %}
 
        {% if no_cookie %}
 
          <p>{{ no_cookie1 }}</p>
 
          <p>{{ no_cookie2 }}</p>
 
        {% endif %}
 

	
 
        {% if DEBUG %}
 
          <h2>Help</h2>
 
            {% if reason %}
 
            <p>Reason given for failure:</p>
 
            <pre>
 
            {{ reason }}
 
            </pre>
 
            {% endif %}
 
          <p>In general, this can occur when there is a genuine Cross Site Request Forgery, or when
 
          <a
 
          href="https://docs.djangoproject.com/en/{{ docs_version }}/ref/csrf/">Django's
 
          CSRF mechanism</a> has not been used correctly.  For POST forms, you need to
 
          ensure:</p>
 
          <ul>
 
            <li>Your browser is accepting cookies.</li>
 
            <li>The view function passes a <code>request</code> to the template's <a
 
            href="https://docs.djangoproject.com/en/dev/topics/templates/#django.template.backends.base.Template.render"><code>render</code></a>
 
            method.</li>
 
            <li>In the template, there is a <code>{% templatetag openblock %} csrf_token
 
            {% templatetag closeblock %}</code> template tag inside each POST form that
 
            targets an internal URL.</li>
 
            <li>If you are not using <code>CsrfViewMiddleware</code>, then you must use
 
            <code>csrf_protect</code> on any views that use the <code>csrf_token</code>
 
            template tag, as well as those that accept the POST data.</li>
 
          </ul>
 
          <p>You're seeing the help section of this page because you have <code>DEBUG =
 
          True</code> in your Django settings file. Change that to <code>False</code>,
 
          and only the initial error message will be displayed.  </p>
 
          <p>You can customize this page using the CSRF_FAILURE_VIEW setting.</p>
 
        {% else %}
 
          <p><small>{{ more }}</small></p>
 
        {% endif %}
 
      </div>
 
    </div>
 
  {% endblock %}
 
{% endblock %}
pinaxcon/templates/404.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% load i18n %}
 

	
 
{% block body_class %}template-blogpage{% endblock %}
 

	
 
{% block head_title %}{{ page.title }}{% endblock %}
 

	
 
{% block body %}
 
  {% block content %}
 
    <div class="l-content-page">
 
      <div class="l-content-page--richtext ooops-hack">
 
        <h2>Ooops</h2>
 

	
 
        <p>The page you're looking for doesn't exist. Sorry!</p>
 
      </div>
 
    </div>
 
  {% endblock %}
 
{% endblock %}
pinaxcon/templates/dashboard.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 
{% load i18n %}
 

	
 
{% load review_tags %}
 
{% load teams_tags %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load staticfiles %}
 

	
 

	
 
{% block head_title %}Dashboard{% endblock %}
 
{% block page_title %}User Dashboard{% endblock %}
 

	
 
{% block alert %}
 
{% endblock %}
 

	
 
{% block content %}
 

	
 
{% available_categories as categories %}
 
{% if categories %}
 
  {% include "symposion/dashboard/_categories.html" %}
 
{% endif %}
 

	
 
<div class="mb-4">
 
  {% include "symposion/dashboard/speaking.html" %}
 
</div>
 

	
 
{% if review_sections %}
 
<div class="mb-4">
 
  <div class="row">
 
    <h2 class="col-12 mb-3">{% trans "Reviews" %}</h2>
 
  </div>
 
  <div class="row">
 
    {% for section in review_sections %}
 
    <div class="col-md-6 col-lg-4">
 
    <div class="card card-default mb-3">
 
      <div class="card-header">
 
          <h3 class="card-title">{{ section }}</h3>
 
      </div>
 
      <div class="card-body">
 
        <ul class="list-unstyled">
 
            <li><a href="{% url "review_section" section.section.slug %}">All Reviews</a></li>
 
            {% comment %}
 
            <li><a href="{% url "review_section_assignments" section.section.slug %}">Your Assignments</a></li>
 
            {% endcomment %}
 
            <li><a href="{% url "user_reviewed" section.section.slug %}">Reviewed by you</a></li>
 
            <li><a href="{% url "user_not_reviewed" section.section.slug %}">Not Reviewed by you</a></li>
 
            <li><a href="{% url "user_random" section.section.slug %}">Random unreviewed proposal</a></li>
 
          </ul>
 

	
 
          <ul class="list-unstyled">
 
            <li><a href="{% url "review_status" section.section.slug %}">Voting Status</a></li>
 
            {% if section in manage_sections %}
 
            <li><a href="{% url "review_bulk_update" section.section.slug %}">Bulk Update</a></li>
 
            <li><a href="{% url "result_notification" section.section.slug "accepted" %}">Result notifications</a></li>
 
            <li><a href="{% url "review_admin" section.section.slug %}">Reviewer Stats</a></li>
 
            {% endif %}
 
          </ul>
 
      </div>
 
    </div>
 
    </div>
 
    {% endfor %}
 
  </div>
 
</div>
 
{% endif %}
 

	
 
{% available_teams as available_teams %}
 
{% if user.memberships.exists or available_teams %}
 
<div class="mb-4">
 
  <div class="row">
 
    <h2 class="col-12 mb-3">{% trans "Teams "%}</h2>
 
  </div>
 
  {% if user.memberships.exists %}
 
  <div class="row">
 
    <h3 class="col-12">Your Teams</h3>
 
  </div>
 
  <div class="row">
 
    {% for membership in user.memberships.all %}
 
    <div class="col-md-6">
 
      {% include "symposion/teams/_team_row.html" with team=membership.team %}
 
    </div>
 
    {% endfor %}
 
  </div>
 
  {% endif %}
 
  {% if available_teams %}
 
  <div class="row">
 
    <h3 class="col-12">Available Teams</h3>
 
  </div>
 
  <div class="row">
 
    {% for team in available_teams %}
 
    <div class="col-md-6">
 
      {% include "symposion/teams/_team_row.html" %}
 
    </div>
 
    {% endfor %}
 
  </div>
 
  {% endif %}
 
</div>
 
{% endif %}
 

	
 
{% endblock %} <!-- block content -->
 

	
 
{% block scripts_extra %}
 
<script type="text/javascript">
 
  var _toggleVoidInvoices = function() {
pinaxcon/templates/raffle.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% block head_title %}Raffle Tickets{% endblock %}
 
{% block page_title %}Raffle Tickets{% endblock %}
 

	
 
{% block content %}
 
<p>
 
  All of the raffles you are entered into and the tickets you have for each of them.
 
  If available, you will be able to purchase additional raffle tickets from the <a href="{% url "dashboard" %}">Dashboard</a>.
 
</p>
 

	
 
<h2>Your Raffle Tickets</h2>
 
{% for raffle in raffles %}
 
{% if raffle.tickets %}
 
  <h3 class="mt-3">{{ raffle }}</h3>
 
  <ul>
 
   {% for id, numbers in raffle.tickets %}
 
    <li>
 
      <strong>Ticket {{ id }}</strong>
 
      <p>{% for number in numbers %}{{ number }}{% if not forloop.last %}, {% endif %}{% endfor %}</p>
 
    </li>
 
  {% endfor %}
 
  </ul>
 
{% endif %}
 
{% empty %}
 
<p>You do not have tickets in any raffles.</p>
 
{% endfor %}
 
{% endblock %}
pinaxcon/templates/raffle_draw.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% block head_title %}Raffle Winners{% endblock %}
 
{% block page_title %}Raffle Winners{% endblock %}
 

	
 
{% block content %}
 
{% for raffle in raffles %}
 
{% if raffle.hidden %}
 
{% else %}
 
<h2 class="mt-3"><a href="{% url "raffle-draw" raffle.id %}">{{ raffle }}</a></h2>
 

	
 
<dl class="row my-4">
 
  {% for prize in raffle.prizes.all %}
 
  <dt class="col-sm-3 text-truncate">{{ prize }}</dt>
 
  <dd class="col-sm-9">
 
    {% if prize.winning_ticket %}
 
    {% with prize.winning_ticket as winner %}
 
    {# this should be attendee name #}
 
    {% with winner.lineitem.invoice.user.attendee.attendeeprofilebase as profile %}
 
    <p><strong>Winning ticket {{ winner.ticket }}, <a href="{% url "attendee" winner.lineitem.invoice.user.id %}">{{ profile.attendee_name }}</a></strong><br />
 
      Drawn by {{ winner.draw.drawn_by }}, {{ winner.draw.drawn_time}}
 
    </p>
 
    {% endwith %}
 
    <div class="alert alert-danger">
 
      <form method="POST" action="{% url 'raffle-redraw' winner.id %}">
 
        {% csrf_token %}
 
        {# This should have a `reason` field that can be passed through to the Audit log #}
 
        <p>
 
          Re-draw <em>{{ prize }}</em>
 
          <button type="submit" class="btn btn-danger float-right">Re-draw</button>
 
        </p>
 
        <div class="clearfix"></div>
 
      </form>
 
    </div>
 
    {% endwith %}
 
    {% else %}
 
    Not drawn
 
    {% endif %}
 
  </dd>
 
  {% endfor %}
 
</dl>
 

	
 
{% if raffle.is_open %}
 
<form method="POST" action="{% url 'raffle-draw' raffle_id=raffle.id %}">
 
    {% csrf_token %}
 
    <button type="submit" class="btn btn-success">Draw tickets</button>
 
    <div class="clearfix"></div>
 
  </form>
 
{% endif %}
 
{% if not forloop.last %}<hr>{% endif %}
 
{% endif %}
 
{% endfor %}
 

	
 
{% endblock %}
pinaxcon/templates/registrasion/base.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 
{% load lca2018_tags %}
 
{% load i18n %}
 

	
 
{% block head_title %}{% block page_title %}{% endblock %}{% endblock %}
 

	
 
{% block content_base %}
 
{% block content %}
 
  <div class="jumbotron rego-content">
 
    {% block proposals_body %}
 
    {% endblock %}
 
  </div>
 
{% endblock content %}
 
{% endblock %}
pinaxcon/templates/registrasion/invoice.html
Show inline comments
 
{% extends "registrasion/base.html" %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% block head_title %}Tax Invoice/Statement #{{ invoice.id }}{% endblock %}
 
{% block page_title %}{% conference_name %}{% endblock %}
 

	
 
{% block proposals_body %}
 
{% include "registrasion/_invoice_details.html" %}
 
<div class="d-print-none mb-4 pb-4">
 
  {% if invoice.is_unpaid %}
 
  <p>
 
    <strong>NOTICE:</strong> The above invoice is automatically generated, and
 
    will be voided if you amend your selections before payment, or if discounts
 
    or products contained in the invoice become unavailable. The products and
 
    discounts are only reserved until the invoice due time, please pay before then
 
    to guarantee your selection. Late payments are accepted only if the products
 
    and discounts are still available.</p>
 

	
 
  {% url "invoice_access" invoice.user.attendee.access_code as access_url %}
 
  <p>Your most recent unpaid invoice will be available at
 
    <a href="{{ access_url }}">{{ request.scheme }}://{{ request.get_host }}{{ access_url }}</a>
 
    You can print that page for your records, or give this URL to your accounts department to pay for this invoice
 
  </p>
 

	
 
  <a class="btn btn-primary" href='{% url "registripe_card" invoice.id invoice.user.attendee.access_code %}'>Pay this invoice by card</a>
 

	
 
  {% if user.is_staff %}
 
  <a class="btn btn-secondary" href="{% url "manual_payment" invoice.id %}">Apply manual payment</a>
 
  {% endif %}
 

	
 
  {% elif invoice.is_paid %}
 
  {% if user.is_staff %}
 
  <a class="btn btn-primary" href="{% url "manual_payment" invoice.id %}">Apply manual payment/refund</a>
 
  <a class="btn btn-secondary" href="{% url "refund" invoice.id %}">Refund by issuing credit note</a>
 
  {% endif %}
 
  {% endif %}
 

	
 
  {% if user.is_staff %}
 
  <a class="btn btn-info" href="{% url "attendee" invoice.user.id %}">View attendee</a>
 
  <a class="btn btn-light" href="{% url "invoice_update" invoice.id %}">Refresh recipient</a>
 
  {% endif %}
 
</div>
 

	
 
{% endblock %}
pinaxcon/templates/site_base.html
Show inline comments
 
{% load staticfiles %}
 
{% load static %}
 
{% load i18n %}
 
{% load sitetree %}
 
{% load sass_tags %}
 
{% load capture_tags %}
 

	
 
{% capture as head_title silent %}{% block head_title_base %}{% if SITE_NAME %}{{ SITE_NAME }} | {% endif %}{% block head_title %}{% endblock %}{% endblock %}{% endcapture %}
 

	
 
<!DOCTYPE html>
 

	
 
<html lang="en">
 
<head>
 
  <meta charset="utf-8">
 
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
 
  <meta name="description" content="">
 
  <meta name="viewport" content="width=device-width, initial-scale=1">
 

	
 
  <title>{{ head_title }}</title>
 

	
 
  <meta property="og:type" content="website" />
 

	
 
  <!-- Cards -->
 
  <meta property="og:title" content="{{ head_title }}">
 
  <meta property="og:description" content="linux.conf.au 2021 - Jan 23-25 2021, Online, Worldwide" />
 
  <meta property="og:url" content="{{ request.scheme }}://{{ request.get_host }}{{ request.path }}">
 
  <meta name="twitter:site" content="@linuxconfau">
 
  <meta name="twitter:image:alt" content="{{ head_title }}" />
 
  <meta name="twitter:card" content="summary">
 
  <meta name="twitter:image" content="{{ request.scheme }}://{{ request.get_host }}/media/img/card/lca_badge.0e10614e.png" />
 
  <meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}/media/img/card/lca_badge.0e10614e.png" />
 
  <meta property="og:image:width" content="400" />
 
  <meta property="og:image:height" content="400" />
 

	
 
  {% block styles %}
 
  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.6.3/css/all.css" integrity="sha384-UHRtZLI+pbxtHCWp1t77Bi1L4ZtiqrqD80Kn4Z8NTSRyMA2Fd33n5dQ8lWUE00s/" crossorigin="anonymous">
 
  <link href="{% sass_src 'scss/app.scss' %}" rel="stylesheet" type="text/css" />
 
  {% block extra_style %}{% endblock %}
 
  {% endblock %}
 

	
 
  {% block extra_head_base %}
 
  {% block extra_head %}{% endblock %}
 
  {% endblock %}
 
</head>
 
<body class="{% block body_class %}{% endblock %}">
 
  {% block template_overrides %}{% endblock %}
 
  <header class="clearfix d-print-none">
 
    {% block alert %}{% endblock %}
 
    {% block navbar %}{% include 'nav.html' %}{% endblock %}
 
  </header>
 

	
 
  {% if messages %}
 
  <div class="container my-5 alert alert-primary d-print-none">
 
    <ul class="messagelist list-unstyled">
 
      {% for message in messages %}
 
      <li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
 
      {% endfor %}
 
    </ul>
 
  </div>
 
  {% endif %}
 

	
 
  <main role="main" class="{% block main_class %}container{% endblock %}">
 
    <div class="row">
 
      <div class="col page-header">
 
        <h1 class="page-title">{% block page_title %}{% endblock %}</h1>
 
        <p class="lead">{% block page_lead %}{% endblock %}</p>
 
      </div>
 
    </div>
 

	
 
    {% block body_base %}
 
    {% block body_out %}
 
    <div class="row">
 
      <div class="col-md-12">
 
        {% block body_outer %}
 
        {% endblock %}
 
      </div>
 
    </div>
 
    {% block content %}
 
    {% endblock %}
 
    {% endblock %}
 
    {% endblock %}
 

	
 
    {% block footer_base %}
 
    {% block footer %}
 
    {% endblock %}
 
    {% endblock %}
 

	
 
    {% block scripts %}
 
    <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
 
    <script src="{% static 'js/app.js' %}" type="text/javascript"></script>
 
    <script src="{% static 'js/jquery.formset.js' %}"></script>
 
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
 
    <script src="{% static 'bootstrap/js/bootstrap.bundle.min.js' %}"></script>
 
    {% if settings.ANALYTICS_KEY %}
 
    <!-- Global site tag (gtag.js) - Google Analytics -->
 
    <script async src="https://www.googletagmanager.com/gtag/js?id={{ settings.ANALYTICS_KEY }}"></script>
 
    <script>
 
      window.dataLayer = window.dataLayer || [];
pinaxcon/templates/symposion/dashboard/_categories.html
Show inline comments
 
{% load i18n %}
 
{% load proposal_tags %}
 
{% load review_tags %}
 
{% load teams_tags %}
 
{% load registrasion_tags %}
 
{% load lca2018_tags %}
 
{% load lca2019_tags %}
 
{% load staticfiles %}
 
{% load static %}
 
{% load waffle_tags %}
 

	
 
{% if user.is_staff %}
 
<div class="mb-4">
 
  <div class="row">
 
    <div class="col-12">
 
      <h2>{% trans "Administration" %}</h2>
 
      <p>The following administrative tools are available to you:
 
        <ul class="list-unstyled">
 
          <li><a href="{% url "reports_list" %}">Reports</a></li>
 
        </ul>
 
      </p>
 
    </div>
 
  </div>
 
</div>
 
{% endif %}
 

	
 
<div class="mb-4">
 
  <div class="row">
 
    <div class="col-12">
 
      <h2>{% trans "Attend" %} {% conference_name %}</h2>
 
    </div>
 
  </div>
 

	
 
  {% if not user.attendee.completed_registration %}
 
  <div class="row">
 
    <div class="col-12">
 
      <h3>Register</h3>
 
      <p>To attend the conference, you must create an attendee profile and purchase your ticket</p>
 
      <div>
 
        <a class="btn btn-lg btn-primary" role="button" href="{% url "guided_registration" %}">Get your ticket</a>
 
      </div>
 
    </div>
 
  </div>
 
  {% else %}
 
  <div class="row">
 
    <div class="col-md-6 mb-3 mb-md-0">
 
      <h3>Attendee Profile</h3>
 
      <p>If you would like to change the details on your badge or your attendee statistics, you may edit your attendee profile here.</p>
 
      <div>
 
        <a class="btn btn-primary" role="button" href="{% url "attendee_edit" %}">Edit attendee profile</a>
 
        {% flag "badge_preview" %}
 
        <a class="btn btn-info" role="button" href="{% url "user_badge" %}">Preview my badge</a>
 
        {% endflag %}
 
      </div>
 
    </div>
 
    <div class="col-md-6 mb-3 mb-md-0">
 
      <h3>Account Management</h3>
 
      <p>If you would like to change your registered email address or password, you can use our self-service account management portal</p>
 
      <div>
 
        <a class="btn btn-primary" role="button" href="https://login.linux.conf.au/manage/">Account Management</a>
 
      </div>
 
    </div>
 
  </div>
 

	
 
  {% items_pending as pending %}
 
  <div class="row">
 
    <div class="col-12">
 
      <h3 class="my-3">Account</h3>
 
    </div>
 
  </div>
 

	
 
  <div class="row">
 
    {% if pending %}
 
    <div class="col-md-6 mb-3">
 
      <h4>Items pending payment</h4>
 
      {% include "registrasion/_items_list.html" with items=pending %}
 
      <a class="btn btn-primary" role="button" href="{% url "checkout" %}"><i class="fa fa-credit-card"></i> Check out and pay</a>
 
      <a class="btn btn-secondary" role="button" href="{% url "voucher_code" %}">Apply voucher</a>
 
    </div>
 
    {% endif %}
 

	
 
    {% items_purchased as purchased %}
 
    {% if purchased %}
 
    <div class="col-md-6 mb-3">
 
      <h4>Paid Items</h4>
 
      {% include "registrasion/_items_list.html" with items=purchased %}
 

	
 
      {% if not pending %}
 
      <a class="btn btn-secondary" role="button" href="{% url "voucher_code" %}">Apply voucher</a>
 
      {% endif %}
 
    </div>
 
    {% endif %}
 

	
 
    <div class="col-md-6 mb-3">
 
      <h4>Add/Update Items</h4>
 
      {% missing_categories as missing %}
 
      {% if missing %}
 
      <div class="alert alert-warning my-4 pb-4">
 
        <h5 class="alert-heading">You have empty categories</h5>
 
        <p>You have <em>not</em> selected anything from the following
 
          categories. If your ticket includes any of these, you still need to
 
          make a selection:
 
        </p>
 

	
 
        {% include "registrasion/_category_list.html" with categories=missing %}
pinaxcon/templates/symposion/proposals/base.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 

	
 
{% block body_outer %}
 
  {% block proposals_body %}
 
  {% endblock %}
 
{% endblock %}
 

	
pinaxcon/templates/symposion/reviews/base.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% load i18n %}
 

	
 
{% block body_class %}reviews{% endblock %}
 
{% block main_class %}container-fluid{% endblock %}
 

	
 
{% block body_outer %}
 
<div class="container-fluid">
 
    <div class="row">
 
        <div class="col-md-2 col-sm-4">
 
            {% block sidebar %}
 
            {% for section in review_sections %}
 

	
 
            <div class="card card-default mb-3">
 
                <div class="card-header">
 
                    <h3 class="card-title">{{ section }}</h3>
 
                </div>
 
                <div class="list-group">
 
                    <a class="list-group-item review-list" href="{% url "review_section" section.section.slug %}">
 
                        {% trans "All Reviews" %}
 
                    </a>
 
                    {% comment %}
 
                    <a class="list-group-item" href="{% url "review_section_assignments" section.section.slug %}">
 
                        {% trans "Your Assignments" %}
 
                    </a>
 
                    {% endcomment %}
 
                    <a class="list-group-item user-reviewed" href="{% url "user_reviewed" section.section.slug %}">
 
                        {% trans "Reviewed by you" %}
 
                    </a>
 
                    <a class="list-group-item user-not-reviewed" href="{% url "user_not_reviewed" section.section.slug %}">
 
                        {% trans "Not Reviewed by you" %}
 
                    </a>
 
                    <a class="list-group-item user-random" href="{% url "user_random" section.section.slug %}">
 
                        {% trans "Random unreviewed proposal" %}
 
                    </a>
 
                    <a class="list-group-item voting-status" href="{% url "review_status" section.section.slug %}">
 
                        {% trans "Voting Status" %}
 
                    </a>
 
                    {% if section in manage_sections %}
 
                    <a class="list-group-item" href="{% url "review_bulk_update" section.section.slug %}">
 
                        Bulk Update
 
                    </a>
 
                    <a class="list-group-item review-results" href="{% url "result_notification" section.section.slug 'accepted' %}">
 
                        Result Notifications
 
                    </a>
 
                    <a class="list-group-item" href="{% url "review_admin" section.section.slug %}">
 
                        Reviewer Stats
 
                    </a>
 
                    {% endif %}
 
                </div>
 
            </div>
 
            {% endfor %}
 
            {% endblock %}
 
        </div>
 
        <div class="col-md-10 col-sm-8">
 
            {% block body %}
 
            {% endblock %}
 
        </div>
 
    </div>
 
</div>
 
{% endblock %}
 

	
 
{% block extra_style %}
 
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/bs4/jszip-2.5.0/dt-1.10.18/b-1.5.6/b-colvis-1.5.6/b-html5-1.5.6/b-print-1.5.6/cr-1.5.0/fc-3.2.5/fh-3.1.4/kt-2.5.0/r-2.2.2/rg-1.1.0/datatables.min.css"/>
 
{% endblock %}
 

	
 
{% block extra_script %}
 
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/pdfmake.min.js"></script>
 
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.36/vfs_fonts.js"></script>
 
<script type="text/javascript" src="https://cdn.datatables.net/v/bs4/jszip-2.5.0/dt-1.10.18/b-1.5.6/b-colvis-1.5.6/b-html5-1.5.6/b-print-1.5.6/cr-1.5.0/fc-3.2.5/fh-3.1.4/kt-2.5.0/r-2.2.2/rg-1.1.0/datatables.min.js"></script>
 
<script type="text/javascript">
 
    var reviewtable = $("table.table-reviews").DataTable({
 
            "stateSave": true,
 
            "autoWidth": false,
 
            "lengthMenu": [[10, 50, 100, -1], [10, 50, 100, "All"]],
 
            "pageLength": 100,
 
            "colReorder": true,
 
            "columnDefs": [
 
                { targets: [6, 7, 8, 9, 10], visible: false }
 
            ],
 
            "buttons": [{
 
                extend: 'collection',
 
                text: 'Export',
 
                buttons: ["copy", "csv", "print", "pdf"]
 
            },
 
            {
 
                extend: 'collection',
 
                text: 'Columns',
 
                buttons: [
 
                    {
 
                        extend: 'columnsToggle',
 
                        columns: '.toggle'
 
                    },
 
                    {
 
                        extend: 'columnToggle',
 
                        text: 'Vote details',
pinaxcon/templates/symposion/reviews/review_list.html
Show inline comments
 
{% extends "symposion/reviews/base.html" %}
 

	
 
{% block head_title %}Reviews - {{ section }}{% endblock %}
 

	
 
{% block body_class %}{{ block.super }}
 
	{% if reviewed == "all_reviews" %}
 
		review-list
 
	{% endif %}
 
{% endblock %}
 

	
 
{% block body %}
 
	<h3>{{ section }}</h3>
 
	{% if reviewed == 'all_reviews' %}
 
    	<h4>All proposals</h4>
 
    {% elif reviewed == 'user_reviewed' %}
 
    	<h4>Proposals you have reviewed</h4>
 
		<h4>Proposals you have reviewed</h4>
 
	{% elif reviewed == 'user_not_reviewed' %}
 
		<h4>Proposals you have not reviewed</h4>
 
    {% else %}
 
    	<h4>Proposals you have not yet reviewed</h4>
 
    	<h4>Proposals reviewed by selected reviewer</h4>
 
    {% endif %}
 

	
 
    {% include "symposion/reviews/_review_table.html" %}
 
{% endblock %}
pinaxcon/templates/symposion/reviews/review_review.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% load bootstrap %}
 

	
 
{% block body_class %}review{% endblock %}
 

	
 
{% block body %}
 
<div class="l-content-page">
 
<div class="l-content-page--richtext">
 
<div class="rich-text">
 
    <h1>Proposal Review</h1>
 

	
 
    <div class="proposal">
 
        <h2>{{ proposal.title }}</h2>
 

	
 
        <p>
 
            {% if proposal.cancelled %}
 
                Cancelled
 
            {% endif %}
 
        </p>
 

	
 
        <div>
 
            {{ proposal.description }}
 
        </div>
 

	
 
        <p><b>Type</b>: {{ proposal.get_session_type_display }}</p>
 

	
 
        <h3>Abstract</h3>
 
        <div class="abstract">
 
            {{ proposal.abstract_html|safe }}
 
        </div>
 

	
 
        <p><b>Audience level</b>: {{ proposal.get_audience_level_display }}</p>
 

	
 
        <p><b>Submitting speaker</b>: {{ proposal.speaker }}</p> {# @@@ bio? #}
 

	
 
        {% if proposal.additional_speakers.all %}
 
            <p><b>Additional speakers</b>:</p>
 
            <ul>
 
            {% for speaker in proposal.additional_speakers.all %}
 
                {% if speaker.user %}
 
                    <li><b>{{ speaker.name }}</b> &mdash; {{ speaker.email }}</li>
 
                {% else %}
 
                    <li>{{ speaker.email }} &mdash; pending invitation</li>
 
                {% endif %}
 
            {% endfor %}
 
            </ul>
 
        {% endif %}
 

	
 
        <h3>Additional Notes (private from submitter)</h3>
 
        <div class="additional_notes">
 
            {{ proposal.additional_notes }}
 
        </div>
 
    </div>
 

	
 
    <h2>Review</h2>
 

	
 
    <form method="POST" action="{% url "review_review" proposal.pk %}" class="form form-horizontal">
 
        {% csrf_token %}
 
        <fieldset class="inlineLabels">
 
            {{ review_form|bootstrap }}
 
            <div class="form_block">
 
                <input type="submit" value="Submit" />
 
            </div>
 
        </fieldset>
 
    </form>
 

	
 
    <h2>Comment</h2>
 

	
 
    <form method="POST" action="{% url "review_comment" proposal.pk %}" class="form form-horizontal">
 
        {% csrf_token %}
 
        <fieldset>
 
            {{ comment_form|bootstrap }}
 
            <div class="form_block">
 
                <input type="submit" value="Submit" />
 
            </div>
 
        </fieldset>
 
    </form>
 
</div></div></div>
 
{% endblock %}
pinaxcon/templates/symposion/schedule/presentation_detail.html
Show inline comments
 
{% extends "site_base.html" %}
 

	
 
{% load lca2018_tags %}
 
{% load lca2019_tags %}
 
{% load sitetree %}
 
{% load staticfiles %}
 
{% load static %}
 
{% load thumbnail %}
 

	
 
{% block head_title %}Presentation: {{ presentation.title }}{% endblock %}
 
{% block page_title %}{{ presentation.title }}{% endblock %}
 
{% block page_lead %}
 
{% if presentation.slot %}
 
{{ presentation.slot.rooms.0 }} | {{ presentation.slot.day.date|date:"D d M" }} | {{ presentation.slot.start }}&ndash;{{ presentation.slot.end }}
 
{% else %}
 
<em>Not currently scheduled.</em>
 
{% endif %}
 
{% endblock %}
 

	
 
{% block content %}
 
  {% if presentation.unpublish %}
 
  <div class="row">
 
    <div class="col">
 
      <p><strong>Presentation not published.</strong></p>
 
    </div>
 
  </div>
 
  {% endif %}
 

	
 
  <div class="row presentation-details">
 
    <div class="col-md-3">
 
      <h2 class="mt-4">Presented by</h4>
 
      <ul class="list-unstyled">
 
        {% for speaker in presentation.speakers %}
 
        <li class="mb-4 pb-2">
 
          {% speaker_photo speaker 120 as speaker_photo_url %}
 
          <img src="{{ speaker_photo_url }}" alt="{{ speaker }}" class="rounded-circle img-fluid">
 
          <p>
 
            <strong><a href="{% url "speaker_profile" speaker.pk %}">{{ speaker }}</a></strong><br />
 
            {% if speaker.twitter_username %}
 
              <a href="https://twitter.com/{{ speaker.twitter_username }}">{{ speaker.twitter_username|twitter_handle }}</a><br />
 
            {% endif %}
 
            {% if speaker.homepage %}
 
              <a href="{{ speaker.homepage }}">{{ speaker.homepage }}</a>
 
            {% endif %}
 
          </p>
 
          <div class="bio">{{ speaker.biography_html|safe}}</div>
 
          </p>
 
        </li>
 
        {% endfor %}
 
      </ul>
 
    </div>
 

	
 
    <div class="col-md-9 presentation-abstract">
 
      <h2 class="mt-4">Abstract</h4>
 
      {% autoescape off %}
 
      <div class="abstract pb-4"><p>{{ presentation.abstract_html|safe|clean_text|urlize }}</p></div>
 
      {% endautoescape %}
 
    </div>
 
  </div>
 
{% endblock %}
pinaxcon/templates/symposion/schedule/session_detail.html
Show inline comments
 
{% extends "site_base.html" %}
 

	
 
{% load lca2018_tags %}
 
{% load sitetree %}
 
{% load staticfiles %}
 
{% load static %}
 
{% load thumbnail %}
 
{% load i18n %}
 

	
 

	
 
{% block head_title %}Session: {{ session }}{% endblock %}
 

	
 
{% block breadcrumbs %}{% sitetree_breadcrumbs from "main" %}{% endblock %}
 

	
 

	
 
{% block header_title %}Session: {{ session }}{% endblock %}
 

	
 
{% block header_paragraph %}
 

	
 
{% endblock %}
 

	
 
{% block content %}
 

	
 
  <dl class="dl-horizontal">
 
    <dt>{% trans "Session Chair" %}</dt>
 
    <dd>
 
        {% if chair %}
 
            {{ chair.attendee.attendeeprofilebase.attendeeprofile.name }}
 
            {% if request.user == chair %}
 
                <form method="POST">
 
                    {% csrf_token %}
 
                    <input type="hidden" name="role" value="un-chair" />
 
                    <input type="submit" value="Opt out" class="btn btn-info"/>
 
                </form>
 
            {% endif %}
 
        {% else %}
 
            {% if request.user.is_authenticated %}
 
                {% if request.user.attendee.completed_registration %}
 
                    {% if not chair_denied %}
 
                        <form method="POST">
 
                            {% csrf_token %}
 
                            <input type="hidden" name="role" value="chair" />
 
                            <input type="submit" class="btn btn-success" value="Volunteer to be session chair"/>
 
                        </form>
 
                    {% endif %}
 
                {% else %}
 
                    {% url 'guided_registration' as guided_registration %}
 
                    <a href="{{ guided_registration }}">Click here to get a ticket for the conference,</a> and enable volunteering for session roles.
 
                {% endif %}
 
            {% else %}
 
              Sign up and <a href="/saml2/login/?next={{ request.path }}">log in</a> to volunteer to be session chair.
 
            {% endif %}
 
        {% endif %}
 
    </dd>
 
    <dt>{% trans "Session Runner" %}</dt>
 
    <dd>
 
        {% if runner %}
 
          {{ runner.profile.display_name }}
 
        {% else %}
 
          {% blocktrans %}Session runner not assigned.{% endblocktrans %}
 
        {% endif %}
 
    </dd>
 
  </dl>
 

	
 
  <h2>Slots</h2>
 

	
 
  <table class="table">
 
    {% for slot in session.slots.all %}
 
      <tr>
 
          <td>{{ slot }}</td>
 
          <td>{% if slot.content %}<a href="{% url 'schedule_presentation_detail' slot.content.pk %}">{{ slot.content }}</a>{% endif %}</td>
 
      </tr>
 
    {% empty %}
 
      <li>{% trans "No slots in session." %}</li>
 
    {% endfor %}
 
  </table>
 

	
 
{% endblock %}
pinaxcon/templates/symposion/schedule/session_list.html
Show inline comments
 
{% extends "site_base.html" %}
 

	
 
{% load lca2018_tags %}
 
{% load sitetree %}
 
{% load staticfiles %}
 
{% load static %}
 
{% load thumbnail %}
 
{% load i18n %}
 

	
 

	
 
{% block head_title %}Sessions{% endblock %}
 

	
 
{% block breadcrumbs %}{% sitetree_breadcrumbs from "main" %}{% endblock %}
 

	
 
{% block header_title %}Sessions{% endblock %}
 

	
 
{% block header_paragraph %}
 

	
 
{% endblock %}
 

	
 
{% block content %}
 
  <ul class="unstyled">
 
    {% for session in sessions %}
 
      {% if session.sorted_slots %}
 
        <li>
 
          <a href="{% url 'schedule_session_detail' session.pk %}">{% blocktrans %}Session #{% endblocktrans %}{{ forloop.counter }}</a>
 
          <div class="well">
 
            <ul class="unstyled">
 
              {% for role in session.sessionrole_set.all %}
 
                <li>
 
                  <b>{{ role.get_role_display }}</b>: {{ role.user.attendee.attendeeprofilebase.attendeeprofile.name }}
 
                </li>
 
              {% empty %}
 
                <li>
 
                  <a href="{% url 'schedule_session_detail' session.pk %}">{% blocktrans %}No volunteers signed up. Sign up!{% endblocktrans %}</a>
 
                </li>
 
              {% endfor %}
 
            </ul>
 
            <h4>{% trans "Slots" %}</h4>
 
            <table class="table">
 
              {% for slot in session.sorted_slots %}
 
                <tr>
 
                  <td>{{ slot }}</td>
 
                  <td>
 
                    {% if slot.content %}<a href="{% url 'schedule_presentation_detail' slot.content.pk %}">{{ slot.content }}</a>{% endif %}
 
                  </td>
 
                </tr>
 
              {% empty %}
 
                  <tr>{% trans "No slots in session." %}</tr>
 
              {% endfor %}
 
            </table>
 
          </div>
 
        </li>
 
      {% endif %}
 
    {% empty %}
 
      <li>{% trans "No sessions defined." %}</li>
 
    {% endfor %}
 
  </ul>
 

	
 
{% endblock %}
pinaxcon/templatetags/lca2018_tags.py
Show inline comments
 
import hashlib
 

	
 
from decimal import Decimal
 
from django import template
 
from django.conf import settings
 
from django.contrib.staticfiles.templatetags import staticfiles
 
from easy_thumbnails.files import get_thumbnailer
 
from registrasion.templatetags import registrasion_tags
 
from symposion.conference import models as conference_models
 
from symposion.schedule.models import Track
 

	
 
CONFERENCE_ID = settings.CONFERENCE_ID
 
GST_RATE = settings.GST_RATE
 

	
 
register = template.Library()
 

	
 

	
 
@register.assignment_tag()
 
@register.simple_tag()
 
def classname(ob):
 
    return ob.__class__.__name__
 

	
 

	
 
@register.simple_tag(takes_context=True)
 
def can_manage(context, proposal):
 
    return proposal_permission(context, "manage", proposal)
 

	
 

	
 
@register.simple_tag(takes_context=True)
 
def can_review(context, proposal):
 
    return proposal_permission(context, "review", proposal)
 

	
 

	
 
def proposal_permission(context, permname, proposal):
 
    slug = proposal.kind.section.slug
 
    perm = "reviews.can_%s_%s" % (permname, slug)
 
    return context.request.user.has_perm(perm)
 

	
 

	
 
@register.simple_tag(takes_context=True)
 
def speaker_photo(context, speaker, size):
 
    ''' Provides the speaker profile, or else fall back to libravatar or gravatar. '''
 

	
 
    if speaker.photo:
 
        thumbnailer = get_thumbnailer(speaker.photo)
 
        thumbnail_options = {'crop': True, 'size': (size, size)}
 
        thumbnail = thumbnailer.get_thumbnail(thumbnail_options)
 
        return thumbnail.url
 
    else:
 
        email = speaker.user.email.encode("utf-8")
 
        md5sum = hashlib.md5(email.strip().lower()).hexdigest()
 
        fallback_image = ("https://linux.conf.au/site_media/static/lca2017"
 
                          "/images/speaker-fallback-devil.jpg")
 
        url = "https://secure.gravatar.com/avatar/%s?s=%d&d=%s" % (md5sum, size, "mp")
 

	
 
        return url
 

	
 

	
 
@register.simple_tag()
 
def define(value):
 
    return value
 

	
 

	
 
@register.simple_tag()
 
def presentation_bg_number(presentation, count):
 
    return sum(ord(i) for i in presentation.title) % count
 

	
 

	
 
@register.filter()
 
def gst(amount):
 
    value_no_gst = Decimal(amount / (1 + GST_RATE))
 
    return Decimal(amount - value_no_gst).quantize(Decimal('0.01'))
 

	
 

	
 
@register.simple_tag()
 
def conference_name():
 
    return conference_models.Conference.objects.get(id=CONFERENCE_ID).title
 

	
 

	
 
@register.simple_tag()
 
def conference_start_date():
 
    return conference_models.Conference.objects.get(id=CONFERENCE_ID).start_date
 

	
 

	
 
@register.simple_tag()
 
def conference_end_date():
 
    return conference_models.Conference.objects.get(id=CONFERENCE_ID).end_date
 

	
 

	
 
@register.simple_tag()
 
def conference_timezone():
 
    return conference_models.Conference.objects.get(id=CONFERENCE_ID).timezone
 

	
 

	
 
@register.filter()
 
def day_has_tracks(timetable, day):
 
    try:
 
        track_names = day.track_set.all()
 
        has_tracks = True
 
    except Track.DoesNotExist:
 
        has_tracks = False
 
    return len(track_names)
 

	
 

	
 
@register.filter()
 
def trackname(room, day):
 
    if not room:
 
        return None
 

	
 
    try:
 
        track_name = room.track_set.get(day=day).name
 
    except Track.DoesNotExist:
 
        track_name = None
 
    return track_name
 

	
pinaxcon/urls.py
Show inline comments
 
import debug_toolbar
 
from django.conf import settings
 
from django.conf.urls import include, url
 
from django.conf.urls.static import static
 
from django.views.generic import RedirectView
 
from django.views.generic import TemplateView
 
from django.urls import include, path
 
from django.contrib.flatpages.views import flatpage
 

	
 
from django.contrib import admin
 

	
 
import symposion.views
 

	
 

	
 
urlpatterns = [
 
    url(r'^saml2/', include('djangosaml2.urls')),
 
    url(r"^admin/", include(admin.site.urls)),
 
    path('saml2/', include('djangosaml2.urls')),
 
    path('admin/', admin.site.urls),
 

	
 
    url(r"^speaker/", include("symposion.speakers.urls")),
 
    url(r"^proposals/", include("symposion.proposals.urls")),
 
    url(r"^reviews/", include("symposion.reviews.urls")),
 
    url(r"^schedule/", include("symposion.schedule.urls")),
 
    url(r"^conference/", include("symposion.conference.urls")),
 
    path("speaker/", include("symposion.speakers.urls")),
 
    path("proposals/", include("symposion.proposals.urls")),
 
    path("reviews/", include("symposion.reviews.urls")),
 
    path("schedule/", include("symposion.schedule.urls")),
 
    path("conference/", include("symposion.conference.urls")),
 

	
 
    url(r"^teams/", include("symposion.teams.urls")),
 
    url(r'^raffle/', include("pinaxcon.raffle.urls")),
 
    path("teams/", include("symposion.teams.urls")),
 
    path('raffle/', include("pinaxcon.raffle.urls")),
 

	
 
    # Required by registrasion
 
    url(r'^tickets/payments/', include('registripe.urls')),
 
    url(r'^tickets/', include('registrasion.urls')),
 
    url(r'^nested_admin/', include('nested_admin.urls')),
 
    url(r'^checkin/', include('regidesk.urls')),
 
    url(r'^pages/', include('django.contrib.flatpages.urls')),
 

	
 
    url(r'^dashboard/', symposion.views.dashboard, name="dashboard"),
 
    url(r'^boardingpass', RedirectView.as_view(pattern_name="regidesk:boardingpass")),
 
]
 
    path('tickets/payments/', include('registripe.urls')),
 
    path('tickets/', include('registrasion.urls')),
 
    path('nested_admin/', include('nested_admin.urls')),
 
    path('checkin/', include('regidesk.urls')),
 
    path('pages/', include('django.contrib.flatpages.urls')),
 

	
 
    path('dashboard/', symposion.views.dashboard, name="dashboard"),
 
    path('boardingpass', RedirectView.as_view(pattern_name="regidesk:boardingpass")),
 

	
 
if settings.DEBUG:
 
    import debug_toolbar
 
    urlpatterns.insert(0, url(r'^__debug__/', include(debug_toolbar.urls)))
 
    # Debug Toolbar. Always include to ensure tests work.
 
    path('__debug__/', include(debug_toolbar.urls)),
 
]
 

	
 
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)#
...
 
\ No newline at end of file
 
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
pinaxcon/wsgi.py
Show inline comments
 
import os
 

	
 
os.environ.setdefault("DEBUG", "0")
 
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "pinaxcon.settings")
 

	
 
from django.core.wsgi import get_wsgi_application  # noqa
 

	
 
from dj_static import Cling, MediaCling  # noqa
 

	
 
application = Cling(MediaCling(get_wsgi_application()))
 
application = get_wsgi_application()
requirements.txt
Show inline comments
 
asn1crypto==0.24.0
 
bleach==2.1.3
 
cachetools==2.1.0
 
cairocffi==0.8.1
 
CairoSVG==2.1.2
 
certifi==2018.4.16
 
cffi==1.11.5
 
chardet==3.0.4
 
coverage==4.0.3
 
cryptography==2.3
 
cssselect2==0.2.1
 
dataclasses==0.6
 
decorator==4.3.0
 
defusedxml==0.5.0
 
dj-database-url==0.4.2
 
dj-static==0.0.6
 
Django==1.11.25
 
django-appconf==1.0.1
 
Django>=2.2
 
pinax-theme-bootstrap==8.0.1
 
pinax-eventlog[django-lts]==5.1.0
 
django-formset-js==0.5.0
 
whitenoise==5.2.0
 
dj-database-url==0.5.0
 
pylibmc==1.6.1
 
django-debug-toolbar==3.1.1
 
django-bootstrap-form==3.4
 
django-settings-export~=1.2.1
 
django-capture-tag==1.0
 
django-compressor==2.3
 
django-countries==5.3.1
 
django-crispy-forms==1.7.2
 
django-debug-toolbar==1.9.1
 
django-formset-js==0.5.0
 
django-gapc-storage==0.5.1
 
django-ical==1.4
 
django-jquery-js==3.1.1
 
django-model-utils==3.1.2
 
django-nested-admin==2.2.6
 
django-nose==1.4.5
 
django-reversion==1.10.1
 
django-sass-processor==0.7.3
 
django-settings-export==1.2.1
 
django-sitetree==1.10.0
 
django-taggit==0.18.0
 
django-timezone-field==2.1
 
django-waffle==0.14.0
 
djangosaml2==0.17.2
 
easy-thumbnails==2.5
 
future==0.16.0
 
google-api-python-client==1.7.0
 
google-auth==1.5.1
 
google-auth-httplib2==0.0.3
 
html5lib==1.0.1
 
httplib2==0.11.3
 
icalendar==4.0.2
 
idna==2.7
 
jsonfield==2.0.2
 
libsass==0.19.3
 
lxml==4.0.0
 
mysqlclient==1.3.13
 
nose==1.3.7
 
oauth2client==4.1.2
 
Paste==2.0.3
 
Pillow==5.2.0
 
pinax-eventlog==1.1.1
 
pinax-stripe==3.2.1
 
pinax-theme-bootstrap==7.10.2
 
pyasn1==0.4.4
 
pyasn1-modules==0.2.2
 
pycparser==2.18
 
pycryptodomex==3.6.4
 
pylibmc==1.5.1
 
pyOpenSSL==18.0.0
 
pypng==0.0.18
 
PyQRCode==1.2.1
 
pysaml2==4.8.0
 
python-dateutil==2.7.3
 
pytz==2018.4
 
rcssmin==1.0.6
 
repoze.who==2.3
 
requests==2.19.1
 
rjsmin==1.1.0
 
rsa==3.4.2
 
six==1.11.0
 
sqlparse==0.2.4
 
static3==0.7.0
 
stripe==1.38.0
 
tinycss2==0.6.1
 
uritemplate==3.0.0
 
urllib3==1.23
 
uWSGI==2.0.17.1
 
webencodings==0.5.1
 
WebOb==1.8.2
 
zope.interface==4.5.0
 
djangosaml2==0.50.0
 
django-gapc-storage==0.5.2
 
django-waffle==2.0.0
 

	
 
# database
 
mysqlclient==2.0.1
 

	
 
# For testing
 
django-nose==1.4.7
 
coverage==5.3
 
factory_boy==3.1.0
 

	
 
# Symposion reqs
 
django-appconf==1.0.4
 
django-model-utils==4.0.0
 
django-reversion==3.0.8
 
django-sitetree==1.16.0
 
django-taggit==1.3.0
 
django-timezone-field==4.0
 
easy-thumbnails==2.7.0
 
bleach==3.2.1
 
pytz>=2020.1
 
django-ical==1.7.1
 

	
 
# Registrasion reqs
 
django-nested-admin==3.3.2
 
CairoSVG==2.4.2
 

	
 
# Registripe
 
django-countries>=6.1.3
 
pinax-stripe==4.4.0
 
requests==2.24.0
 
stripe==2.55.0
 

	
 
# SASS Compiler and template tags
 
libsass==0.20.1
 
django-sass-processor==0.8.2
 
django-compressor==2.4
 

	
 
django-crispy-forms==1.9.2
vendor/regidesk/regidesk/forms.py
Show inline comments
 
import copy
 
from regidesk import models
 

	
 
from django import forms
 
from django.core.urlresolvers import reverse
 
import functools
 

	
 
from django import forms
 
from django.core.urlresolvers import reverse
 
from django.core.exceptions import ValidationError
 
from django.db.models import F, Q
 
from django.forms import widgets
 
from django.urls import reverse
 
from django.utils import timezone
 

	
 
from django_countries import countries
 
from django_countries.fields import LazyTypedChoiceField
 
from django_countries.widgets import CountrySelectWidget
 

	
vendor/regidesk/regidesk/management/commands/print_badges.py
Show inline comments
 
#!/usr/bin/env python
 

	
 
from django.core.management.base import BaseCommand, CommandError
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from registrasion.views import _convert_img as convert_img
 
from registrasion.views import render_badge_svg
 

	
 
User = get_user_model()
 

	
 

	
 
class Command(BaseCommand):
 

	
 
    def handle(self, *args, **options):
 

	
 
        users = User.objects.filter(
 
            checkin__checked_in_bool=True).filter(
 
            checkin__badge_printed=False
 
        )
 

	
 
        for user in users:
 

	
 
            try:
 
                svg = render_badge_svg(user, overlay=True)
 
                rendered = convert_img(svg, outformat="pdf")
 
                with open("/app/badge_out/{}.pdf".format(user.checkin.checkin_code), "wb") as outfile:
 
                    outfile.write(rendered)
 
                user.checkin.mark_badge_printed()
 
            except Exception:
 
                pass
vendor/regidesk/regidesk/models.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
import base64
 
from datetime import datetime
 
from decimal import Decimal
 
from io import BytesIO
 

	
 
from django.core.exceptions import ValidationError
 
from django.utils import timezone
 

	
 
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.contrib.auth import get_user_model
 
import pyqrcode
 

	
 
from symposion import constants
 
from symposion.text_parser import parse
 
from registrasion.models import commerce
 
from registrasion.util import generate_access_code as generate_code
 

	
 
User = get_user_model()
 

	
 

	
 
class BoardingPassTemplate(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")
 
    html_body = models.TextField(verbose_name="HTML Body",null=True)
 

	
 
    class Meta:
 
        verbose_name = ("Boarding Pass template")
 
        verbose_name_plural = ("Boarding Pass templates")
 

	
 
class BoardingPass(models.Model):
 

	
 
    template = models.ForeignKey(BoardingPassTemplate, null=True, blank=True,
 
                                 on_delete=models.SET_NULL, verbose_name="Template")
 
    created = models.DateTimeField(auto_now_add=True, verbose_name="Created")
 
    sent = models.DateTimeField(null=True, verbose_name="Sent")
 
    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")
 
    html_body = models.TextField(verbose_name="HTML Body", null=True)
 

	
 
    class Meta:
 
        permissions = (
 
            ("view_boarding_pass", "Can view sent boarding passes"),
 
            ("send_boarding_pass", "Can send boarding passes"),
 
        )
 

	
 
    def __unicode__(self):
 
        return self.checkin.attendee.attendeeprofilebase.attendeeprofile.name + ' ' + self.timestamp.strftime('%Y-%m-%d %H:%M:%S')
 

	
 
    @property
 
    def email_args(self):
 
        return (self.subject, self.body, self.from_address, self.user.email)
 

	
 
class CheckIn(models.Model):
 

	
 
    user = models.OneToOneField(User)
 
    user = models.OneToOneField(User, on_delete=models.CASCADE)
 
    boardingpass = models.OneToOneField(BoardingPass, null=True,
 
                                        blank=True, on_delete=models.SET_NULL)
 
    seen = models.DateTimeField(null=True,blank=True)
 
    checked_in = models.DateTimeField(null=True,blank=True)
 
    checkin_code = models.CharField(
 
        max_length=6,
 
        unique=True,
 
        db_index=True,
 
    )
 
    _checkin_code_png=models.TextField(max_length=512,null=True,blank=True)
 
    badge_printed = models.BooleanField(default=False)
 
    schwag_given = models.BooleanField(default=False)
 
    checked_in_bool = models.BooleanField(default=False)
 
    needs_review = models.BooleanField(default=False)
 
    review_text = models.TextField(blank=True)
 

	
 
    class Meta:
 
        permissions = (
 
            ("view_checkin_details", "Can view the details of other user's checkins"),
 
        )
 

	
 
    def save(self, *a, **k):
 
        while not self.checkin_code:
 
            checkin_code = generate_code()
 
            if CheckIn.objects.filter(checkin_code=checkin_code).count() == 0:
 
                self.checkin_code = checkin_code
 
        return super(CheckIn, self).save(*a, **k)
 

	
 
    def mark_checked_in(self):
 
        self.checked_in_bool = True
 
        self.checked_in = timezone.now()
 
        self.save()
 

	
 
    def mark_badge_printed(self):
 
        self.badge_printed = True
 
        self.save()
 

	
 
    def unset_badge(self):
 
        self.badge_printed = False
 
        self.save()
 

	
 
    def mark_schwag_given(self):
 
        self.schwag_given = True
 
        self.save()
 

	
 
    def bulk_mark_given(self):
 
        self.checked_in_bool = True
 
        self.schwag_given = True
 
        self.save()
 

	
 
    def set_exception(self, text):
 
        self.needs_review = True
 
        self.review_text = text
 
        self.save()
 

	
 
    @property
 
    def code(self):
 
        return self.checkin_code
 

	
 
    @property
 
    def qrcode(self):
 
        """Returns the QR Code for this checkin's code.
 

	
 
        If this is the first time the QR code has been generated, cache it on the object.
 
        If a code has already been cached, serve that.
 

	
 
        Returns the raw PNG blob, unless b64=True, in which case the return value
 
        is the base64encoded PNG blob."""
 

	
 
        if not self.code:
 
            return None
 
        if not self._checkin_code_png:
 
            qrcode = pyqrcode.create(self.code)
 
            qr_io = BytesIO()
 
            qrcode.png(qr_io, scale=6)
 
            qr_io.seek(0)
 
            self._checkin_code_png = base64.b64encode(qr_io.read()).decode('UTF-8')
 
            self.save()
 

	
 
        return self._checkin_code_png
vendor/regidesk/regidesk/templates/regidesk/base.html
Show inline comments
 
{% extends "site_base.html" %}
 
{% load staticfiles %}
 
{% load static %}
 

	
 
{% load i18n %}
 

	
 
{% block head_title %}Checkin{% endblock head_title %}
 

	
 
{% block content %}
 
{% block body %}
 
{% endblock %}
 
{% endblock %}
 

	
 
{% block extra_script %}
 
<link rel="stylesheet" type="text/css" href="//cdn.datatables.net/1.10.19/css/jquery.dataTables.min.css" />
 
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/pdfmake.min.js"></script>
 
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.1.32/vfs_fonts.js"></script>
 
<script type="text/javascript" src="//cdn.datatables.net/1.10.19/js/jquery.dataTables.min.js  "></script>
 
<script type="text/javascript">
 
  $("table.table-data").dataTable({
 
    "dom": "<'row'<'col-md-3'l><'col-md-3'B><'col-md-4'f>r>t<'row'<'col-md-3'i><'col-md-5'p>>",
 
    "stateSave": true,
 
    "lengthMenu": [[10, 50, 100, -1], [10, 50, 100, "All"]],
 
    "pageLength": 100,
 
    "colReorder": true,
 
    "buttons": [{
 
      extend: 'collection',
 
      text: 'Export',
 
      buttons: ["copy", "csv", "print"]
 
    },
 
    {
 
      extend: 'collection',
 
      text: 'Columns',
 
      buttons: [
 
        {
 
          extend: 'columnsToggle',
 
          columns: '.toggle'
 
        },
 
      ]
 
    }]
 
  });
 
</script>
 
{% endblock %}
...
 
\ No newline at end of file
 
{% endblock %}
vendor/regidesk/regidesk/views.py
Show inline comments
 
import base64
 
import logging
 
from datetime import datetime
 
from email.mime.image import MIMEImage
 

	
 
from django.core.exceptions import ValidationError
 
from django.core.mail import EmailMultiAlternatives
 
from django.conf import settings
 
from django.contrib import messages
 
from django.contrib.auth.decorators import permission_required, user_passes_test, login_required
 
from django.contrib.auth.mixins import PermissionRequiredMixin
 
from django.contrib.auth.models import User, Group
 
from django.contrib.auth.models import Group
 
from django.contrib.auth import get_user_model
 
from django.contrib.sites.models import Site
 
from django.db import transaction
 
from django.db.models import F, Q
 
from django.db.models import Count, Max, Sum
 
from django.http import Http404
 
from django.http import HttpResponse, HttpResponseBadRequest
 
from django.shortcuts import redirect, render
 
from django.template import Template, Context
 
from django.urls import reverse
 
from django.views.decorators.csrf import csrf_exempt
 
from django.views.generic import TemplateView
 

	
 
from registrasion import util
 
from registrasion.views import render_badge
 
from registrasion.models import commerce, people
 
from registrasion.templatetags.registrasion_tags import items_purchased, items_pending
 
from registrasion.templatetags.registrasion_tags import invoices, missing_categories
 
from symposion.conference.models import Conference
 

	
 
from regidesk import forms
 
from regidesk.models import BoardingPass, BoardingPassTemplate, CheckIn
 

	
 

	
 

	
 
User = get_user_model()
 
AttendeeProfile = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL)
 

	
 

	
 
def _staff_only(user):
 
    ''' Returns true if the user is staff. '''
 
    return user.is_staff
 

	
 
@login_required
 
def boardingpass(request):
 

	
 
    user=request.user
 
    checkin = CheckIn.objects.get_or_create(user=user)[0]
 
    if not checkin.boardingpass:
 
        templates = BoardingPassTemplate.objects.all()
 
        if not templates:
 
            messages.add_message(request, messages.WARNING,
 
                                 'Your boarding pass has not been prepared and I can\'t find a '
 
                                 'default template to use. This page has similar information to '
 
                                 'the boarding pass - please check back later.')
 
            return redirect('/tickets/review')
 
        prepare_boarding_pass(request.user, templates[0])
 
        checkin = CheckIn.objects.get_or_create(user=user)[0]
 

	
 
    boardingpass = checkin.boardingpass
 
    qrcode_url = request.build_absolute_uri(reverse("regidesk:checkin_png", args=[checkin.code]))
 
    qrcode = checkin.qrcode
 
    qrcode_string ='<img src="data:image/png;base64,' + qrcode + '"/>'
 
    not_qrcode_string = '<img src="cid:qrcode.png"/>'
 

	
 
    boardingpass_body = boardingpass.html_body.replace(not_qrcode_string, qrcode_string)
 
    ctx = { 'attendee': user.attendee,
 
            'boardingpass_body': boardingpass_body,
 
            'boardingpass': boardingpass
 
    }
 

	
 
    response = render(request, "regidesk/boardingpass.html", ctx)
 
    return response
 

	
 

	
 
@permission_required("regidesk.view_boarding_pass")
 
def boarding_overview(request, boarding_state="pending"):
 

	
 
    tickets = commerce.LineItem.objects.select_related(
 
        "invoice","invoice__user__attendee","product__category"
 
    ).filter(
 
        invoice__status=commerce.Invoice.STATUS_PAID,
 
        product__category=settings.TICKET_PRODUCT_CATEGORY,
 
        price__gte=0
 
    )
 

	
 
    print(datetime.now())
 
    ticketholders = ( ticket.invoice.user for ticket in tickets )
 
    print(datetime.now())
 

	
 
    attendees = people.Attendee.objects.select_related(
 
            "attendeeprofilebase",
 
            "attendeeprofilebase__attendeeprofile",
 
            "user",
 
            "user__checkin"
 
        ).filter(user__in=ticketholders)
 

	
 
    profiles = AttendeeProfile.objects.filter(
 
        attendee__in=attendees
 
    ).select_related(
 
        "attendee", "attendee__user",
 
    )
 
    profiles_by_attendee = dict((i.attendee, i) for i in profiles)
 

	
 
    bp_templates = BoardingPassTemplate.objects.all()
 

	
 
    ctx = {
 
        "boarding_state": boarding_state,
 
        "attendees": attendees,
 
        "profiles": profiles_by_attendee,
 
        "templates": bp_templates,
 
    }
 

	
 
    return render(request, "regidesk/boardingpass_overview.html", ctx)
 

	
 
def checkin_png(request, checkin_code):
 

	
 
    checkin = CheckIn.objects.get(checkin_code=checkin_code)
 
    if not checkin:
 
        raise Http404()
 

	
 
    response = HttpResponse()
 
    response["Content-Type"] = "image/png"
 
    response["Content-Disposition"] = 'inline; filename="qrcode.png"'
 

	
 
    qrcode = base64.b64decode(checkin.qrcode)
 
    response.write(qrcode)
 

	
 
    return response
 

	
 

	
 
def checkin_qrcode_url(code):
 
    """ Generate the QR code URL for the current site.
 
    eg. https://rego.linux.conf.au/checkin/<code>.png
 
    """
vendor/regidesk/requirements.txt
Show inline comments
 
django-countries>=4.0
 
requests>=2.11.1
 
django-countries>=6.1.3
 
requests>=2.24.0
 
pypng
 
pyqrcode
vendor/registrasion/registrasion/admin.py
Show inline comments
 
from django.contrib import admin
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.utils.translation import ugettext_lazy as _
 

	
 
import nested_admin
 

	
 
from registrasion.models import conditions
 
from registrasion.models import inventory
 

	
 

	
 
class EffectsDisplayMixin(object):
 
    def effects(self, obj):
 
        return list(obj.effects())
 

	
 

	
 
# Inventory admin
 

	
 

	
 
class ProductInline(admin.TabularInline):
 
    model = inventory.Product
 

	
 

	
 
@admin.register(inventory.Category)
 
class CategoryAdmin(admin.ModelAdmin):
 
    model = inventory.Category
 
    fields = ("name", "description", "required", "render_type",
 
              "limit_per_user", "order",)
 
    list_display = ("name", "description")
 
    inlines = [
 
        ProductInline,
 
    ]
 

	
 

	
 
@admin.register(inventory.Product)
 
class ProductAdmin(admin.ModelAdmin):
 
    model = inventory.Product
 
    list_display = ("name", "category", "description")
 
    list_filter = ("category", )
 

	
 

	
 
# Discounts
 

	
 
class DiscountForProductInline(admin.TabularInline):
 
    model = conditions.DiscountForProduct
 
    verbose_name = _("Product included in discount")
 
    verbose_name_plural = _("Products included in discount")
 
    sortable_options = []
 

	
 

	
 
class DiscountForCategoryInline(admin.TabularInline):
 
    model = conditions.DiscountForCategory
 
    verbose_name = _("Category included in discount")
 
    verbose_name_plural = _("Categories included in discount")
 
    sortable_options = []
 

	
 

	
 
@admin.register(conditions.TimeOrStockLimitDiscount)
 
class TimeOrStockLimitDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
 
    list_display = (
 
        "description",
 
        "start_time",
 
        "end_time",
 
        "limit",
 
        "effects",
 
    )
 
    ordering = ("start_time", "end_time", "limit")
 

	
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
@admin.register(conditions.IncludedProductDiscount)
 
class IncludedProductDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
 

	
 
    def enablers(self, obj):
 
        return list(obj.enabling_products.all())
 

	
 
    list_display = ("description", "enablers", "effects")
 

	
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
@admin.register(conditions.SpeakerDiscount)
 
class SpeakerDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
 

	
 
    fields = ("description", "is_presenter", "is_copresenter", "proposal_kind")
 

	
 
    list_display = ("description", "is_presenter", "is_copresenter", "effects")
 

	
 
    ordering = ("-is_presenter", "-is_copresenter")
 

	
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
@admin.register(conditions.GroupMemberDiscount)
 
class GroupMemberDiscountAdmin(admin.ModelAdmin, EffectsDisplayMixin):
 

	
 
    fields = ("description", "group")
 

	
 
    list_display = ("description", "effects")
 

	
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
# Vouchers
 

	
 
class VoucherDiscountInline(nested_admin.NestedStackedInline):
 
    model = conditions.VoucherDiscount
 
    verbose_name = _("Discount")
 

	
 
    # TODO work out why we're allowed to add more than one?
 
    max_num = 1
 
    extra = 1
 
    inlines = [
 
        DiscountForProductInline,
 
        DiscountForCategoryInline,
 
    ]
 

	
 

	
 
class VoucherFlagInline(nested_admin.NestedStackedInline):
 
    model = conditions.VoucherFlag
 
    verbose_name = _("Product and category enabled by voucher")
 
    verbose_name_plural = _("Products and categories enabled by voucher")
 

	
 
    # TODO work out why we're allowed to add more than one?
 
    max_num = 1
 
    extra = 1
 

	
 

	
 
@admin.register(inventory.Voucher)
 
class VoucherAdmin(nested_admin.NestedAdmin):
 
class VoucherAdmin(nested_admin.NestedModelAdmin):
 

	
 
    def effects(self, obj):
 
        ''' List the effects of the voucher in the admin. '''
 
        out = []
 

	
 
        try:
 
            voucher_condition = obj.voucherflag.condition
 
        except ObjectDoesNotExist:
 
            voucher_condition = None
 

	
 
        try:
 
            discount_effects = obj.voucherdiscount.effects()
 
        except ObjectDoesNotExist:
 
            discount_effects = None
 

	
 
        try:
 
            enabling_effects = obj.voucherflag.effects()
 
        except ObjectDoesNotExist:
 
            enabling_effects = None
 

	
 
        if discount_effects:
 
            out.append("Discounts: " + str(list(discount_effects)))
 
        if voucher_condition:
 
            out.append("Condition: " + obj.voucherflag.get_condition_display())
 
        if enabling_effects:
 
            out.append("Enables: " + str(list(enabling_effects)))
 

	
 
        return "\n".join(out)
 

	
 
    model = inventory.Voucher
 
    list_display = ("recipient", "code", "effects")
 
    inlines = [
 
        VoucherDiscountInline,
 
        VoucherFlagInline,
 
    ]
 

	
 

	
 
# Enabling conditions
 
@admin.register(conditions.ProductFlag)
 
class ProductFlagAdmin(
 
        nested_admin.NestedAdmin,
 
        nested_admin.NestedModelAdmin,
 
        EffectsDisplayMixin):
 

	
 
    def enablers(self, obj):
 
        return list(obj.enabling_products.all())
 

	
 
    model = conditions.ProductFlag
 
    fields = ("description", "enabling_products", "condition", "products",
 
              "categories"),
 

	
 
    list_display = ("description", "condition", "enablers", "effects")
 

	
 

	
 
# Enabling conditions
 
@admin.register(conditions.CategoryFlag)
 
class CategoryFlagAdmin(
 
        nested_admin.NestedAdmin,
 
        nested_admin.NestedModelAdmin,
 
        EffectsDisplayMixin):
 

	
 
    model = conditions.CategoryFlag
 
    fields = ("description", "enabling_category", "condition", "products",
 
              "categories"),
 

	
 
    list_display = ("description", "condition", "enabling_category", "effects")
 
    ordering = ("enabling_category",)
 

	
 

	
 
@admin.register(conditions.SpeakerFlag)
 
class SpeakerFlagAdmin(nested_admin.NestedAdmin, EffectsDisplayMixin):
 
class SpeakerFlagAdmin(nested_admin.NestedModelAdmin, EffectsDisplayMixin):
 

	
 
    model = conditions.SpeakerFlag
 
    fields = ("description", "is_presenter", "is_copresenter", "proposal_kind",
 
              "products", "categories")
 

	
 
    list_display = ("description", "is_presenter", "is_copresenter", "effects")
 

	
 
    ordering = ("-is_presenter", "-is_copresenter")
 

	
 

	
 
@admin.register(conditions.GroupMemberFlag)
 
class GroupMemberFlagAdmin(admin.ModelAdmin, EffectsDisplayMixin):
 

	
 
    fields = ("description", "group", "products", "categories")
 

	
 
    list_display = ("description", "condition", "effects")
 

	
 

	
 
@admin.register(conditions.TimeOrStockLimitFlag)
 
class TimeOrStockLimitFlagAdmin(admin.ModelAdmin, EffectsDisplayMixin):
 
    list_display = (
 
        "description",
 
        "condition",
 
        "start_time",
 
        "end_time",
 
        "limit",
 
        "effects",
 
    )
 
    ordering = ("start_time", "end_time", "limit")
vendor/registrasion/registrasion/contrib/badger.py
Show inline comments
 
'''
 
Generate Conference Badges
 
==========================
 

	
 
Nearly all of the code in this was written by Richard Jones for the 2016 conference.
 
That code relied on the user supplying the attendee data in a CSV file, which Richard's
 
code then processed.
 

	
 
The main (and perhaps only real) difference, here, is that the attendee data are taken
 
directly from the database.  No CSV file is required.
 

	
 
This is now a library with functions / classes referenced by the generate_badges
 
management command, and by the tickets/badger and tickets/badge API functions.
 
'''
 
import sys
 
import os
 
import csv
 
from lxml import etree
 
import tempfile
 
from copy import deepcopy
 
import subprocess
 

	
 
import pdb
 

	
 
from django.core.management.base import BaseCommand
 

	
 
from django.contrib.auth.models import User, Group
 
from django.contrib.auth.models import Group
 
from django.contrib.auth import get_user_model
 
from django.db.utils import OperationalError, ProgrammingError
 
from pinaxcon.registrasion.models import AttendeeProfile
 
from registrasion.controllers.cart import CartController
 
from registrasion.controllers.invoice import InvoiceController
 
from registrasion.models import Voucher
 
from registrasion.models import Attendee
 
from registrasion.models import Product
 
from registrasion.models import Invoice
 
from symposion.speakers.models import Speaker
 

	
 
User = get_user_model()
 

	
 
# A few unicode encodings ...
 
GLYPH_PLUS = '+'
 
GLYPH_GLASS = u'\ue001'
 
GLYPH_DINNER = u'\ue179'
 
GLYPH_SPEAKER = u'\ue122'
 
GLYPH_SPRINTS = u'\ue254'
 
GLYPH_CROWN = u'\ue211'
 
GLYPH_SNOWMAN = u'\u2603'
 
GLYPH_STAR = u'\ue007'
 
GLYPH_FLASH = u'\ue162'
 
GLYPH_EDU = u'\ue233'
 

	
 
# Some company names are too long to fit on the badge, so, we
 
# define abbreviations here.
 
overrides = {
 
 "Optiver Pty. Ltd.": "Optiver",
 
 "IRESS Market Tech": "IRESS",
 
 "The Bureau of Meteorology": "BoM",
 
 "Google Australia": "Google",
 
 "Facebook Inc.": "Facebook",
 
 "Rhapsody Solutions Pty Ltd": "Rhapsody Solutions",
 
 "PivotNine Pty Ltd": "PivotNine",
 
 "SEEK Ltd.": "SEEK",
 
 "UNSW Australia": "UNSW",
 
 "Dev Demand Co": "Dev Demand",
 
 "Cascode Labs Pty Ltd": "Cascode Labs",
 
 "CyberHound Pty Ltd": "CyberHound",
 
 "Self employed Contractor": "",
 
 "Data Processors Pty Lmt": "Data Processors",
 
 "Bureau of Meterology": "BoM",
 
 "Google Australia Pty Ltd": "Google",
 
 # "NSW Rural Doctors Network": "",
 
 "Sense of Security Pty Ltd": "Sense of Security",
 
 "Hewlett Packard Enterprose": "HPE",
 
 "Hewlett Packard Enterprise": "HPE",
 
 "CISCO SYSTEMS INDIA PVT LTD": "CISCO",
 
 "The University of Melbourne": "University of Melbourne",
 
 "Peter MacCallum Cancer Centre": "Peter Mac",
 
 "Commonwealth Bank of Australia": "CBA",
 
 "VLSCI, University of Melbourne": "VLSCI",
 
 "Australian Bureau of Meteorology": "BoM",
 
 "Bureau of Meteorology": "BoM",
 
 "Australian Synchrotron | ANSTO": "Australian Synchrotron",
 
 "Bureau of Meteorology, Australia": "BoM",
 
 "QUT Digital Media Research Centre": "QUT",
 
 "Dyn - Dynamic Network Services Inc": "Dyn",
 
 "The Australian National University": "ANU",
 
 "Murdoch Childrens Research Institute": "MCRI",
 
 "Centenary Institute, University of Sydney": "Centenary Institute",
 
 "Synchrotron Light Source Australia Pty Ltd": "Australian Synchrotron",
 
 "Australian Communication and Media Authority": "ACMA",
 
 "Dept. of Education - Camden Haven High School": "Camden Haven High School",
 
 "Australian Government - Bureau of Meteorology": "BoM",
 
 "The Walter and Eliza Hall Institute of Medical Research": "WEHI",
 
 "Dept. Parliamentary Services, Australian Parliamentary Library": "Dept. Parliamentary Services",
 
}
 

	
 

	
 
def text_size(text, prev=9999):
 
    '''
 
    Calculate the length of a text string as it relates to font size.
 
    '''
 
    n = len(text)
 
    size = int(min(48, max(28, 28 + 30 * (1 - (n-8) / 11.))))
 
    return min(prev, size)
 

	
 

	
 
def set_text(soup, text_id, text, resize=None):
 
    '''
 
    Set the text value of an element (via beautiful soup calls).
 
    '''
 
    elem = soup.find(".//*[@id='%s']/{http://www.w3.org/2000/svg}tspan" % text_id)
 
    if elem is None:
 
        raise ValueError('could not find tag id=%s' % text_id)
 
    elem.text = text
 
    if resize:
 
        style = elem.get('style')
 
        elem.set('style', style.replace('font-size:60px', 'font-size:%dpx' % resize))
 

	
 

	
 
def set_colour(soup, slice_id, colour):
 
    '''
 
    Set colour of an element (using beautiful soup calls).
 
    '''
 
    elem = soup.find(".//*[@id='%s']" % slice_id)
 
    if elem is None:
 
        raise ValueError('could not find tag id=%s' % slice_id)
 
    style = elem.get('style')
 
    elem.set('style', style.replace('fill:#316a9a', 'fill:#%s' % colour))
 

	
 
## It's possible that this script will be run before the database has been populated
 
try:
 
    Volunteers = Group.objects.filter(name='Conference volunteers').first().user_set.all()
 
    Organisers = Group.objects.filter(name='Conference organisers').first().user_set.all()
 
except (OperationalError, AttributeError, ProgrammingError):
 
    Volunteers = []
vendor/registrasion/registrasion/controllers/batch.py
Show inline comments
 
import contextlib
 
import functools
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 

	
 
User = get_user_model()
 

	
 

	
 
class BatchController(object):
 
    ''' Batches are sets of operations where certain queries for users may be
 
    repeated, but are also unlikely change within the boundaries of the batch.
 

	
 
    Batches are keyed per-user. You can mark the edge of the batch with the
 
    ``batch`` context manager. If you nest calls to ``batch``, only the
 
    outermost call will have the effect of ending the batch.
 

	
 
    Batches store results for functions wrapped with ``memoise``. These results
 
    for the user are flushed at the end of the batch.
 

	
 
    If a return for a memoised function has a callable attribute called
 
    ``end_batch``, that attribute will be called at the end of the batch.
 

	
 
    '''
 

	
 
    _user_caches = {}
 
    _NESTING_KEY = "nesting_count"
 

	
 
    @classmethod
 
    @contextlib.contextmanager
 
    def batch(cls, user):
 
        ''' Marks the entry point for a batch for the given user. '''
 

	
 
        cls._enter_batch_context(user)
 
        try:
 
            yield
 
        finally:
 
            # Make sure we clean up in case of errors.
 
            cls._exit_batch_context(user)
 

	
 
    @classmethod
 
    def _enter_batch_context(cls, user):
 
        if user not in cls._user_caches:
 
            cls._user_caches[user] = cls._new_cache()
 

	
 
        cache = cls._user_caches[user]
 
        cache[cls._NESTING_KEY] += 1
 

	
 
    @classmethod
 
    def _exit_batch_context(cls, user):
 
        cache = cls._user_caches[user]
 
        cache[cls._NESTING_KEY] -= 1
 

	
 
        if cache[cls._NESTING_KEY] == 0:
 
            cls._call_end_batch_methods(user)
 
            del cls._user_caches[user]
 

	
 
    @classmethod
 
    def _call_end_batch_methods(cls, user):
 
        cache = cls._user_caches[user]
 
        ended = set()
 
        while True:
 
            keys = set(cache.keys())
 
            if ended == keys:
 
                break
 
            keys_to_end = keys - ended
 
            for key in keys_to_end:
 
                item = cache[key]
 
                if hasattr(item, 'end_batch') and callable(item.end_batch):
 
                    item.end_batch()
 
            ended = ended | keys_to_end
 

	
 
    @classmethod
 
    def memoise(cls, func):
 
        ''' Decorator that stores the result of the stored function in the
 
        user's results cache until the batch completes. Keyword arguments are
 
        not yet supported.
 

	
 
        Arguments:
 
            func (callable(*a)): The function whose results we want
 
                to store. The positional arguments, ``a``, are used as cache
 
                keys.
 

	
 
        Returns:
 
            callable(*a): The memosing version of ``func``.
 

	
 
        '''
 

	
 
        @functools.wraps(func)
 
        def f(*a):
 

	
 
            for arg in a:
 
                if isinstance(arg, User):
 
                    user = arg
 
                    break
 
            else:
 
                raise ValueError("One position argument must be a User")
 

	
 
            func_key = (func, tuple(a))
 
            cache = cls.get_cache(user)
 

	
 
            if func_key not in cache:
 
                cache[func_key] = func(*a)
vendor/registrasion/registrasion/controllers/item.py
Show inline comments
 
''' NEEDS TESTS '''
 

	
 
import operator
 
from functools import reduce
 

	
 
from registrasion.models import commerce
 
from registrasion.models import inventory
 

	
 
from collections import Iterable
 
from collections.abc import Iterable
 
from collections import namedtuple
 
from django.db.models import Case
 
from django.db.models import Q
 
from django.db.models import Sum
 
from django.db.models import When
 
from django.db.models import Value
 

	
 
_ProductAndQuantity = namedtuple("ProductAndQuantity", ["product", "quantity"])
 

	
 

	
 
class ProductAndQuantity(_ProductAndQuantity):
 
    ''' Class that holds a product and a quantity.
 

	
 
    Attributes:
 
        product (models.inventory.Product)
 

	
 
        quantity (int)
 

	
 
    '''
 
    pass
 

	
 

	
 
class ItemController(object):
 

	
 
    def __init__(self, user):
 
        self.user = user
 

	
 
    def _items(self, cart_status, category=None):
 
        ''' Aggregates the items that this user has purchased.
 

	
 
        Arguments:
 
            cart_status (int or Iterable(int)): etc
 
            category (Optional[models.inventory.Category]): the category
 
                of items to restrict to.
 

	
 
        Returns:
 
            [ProductAndQuantity, ...]: A list of product-quantity pairs,
 
                aggregating like products from across multiple invoices.
 

	
 
        '''
 

	
 
        if not isinstance(cart_status, Iterable):
 
            cart_status = [cart_status]
 

	
 
        status_query = (
 
            Q(productitem__cart__status=status) for status in cart_status
 
        )
 

	
 
        in_cart = Q(productitem__cart__user=self.user)
 
        in_cart = in_cart & reduce(operator.__or__, status_query)
 

	
 
        quantities_in_cart = When(
 
            in_cart,
 
            then="productitem__quantity",
 
        )
 

	
 
        quantities_or_zero = Case(
 
            quantities_in_cart,
 
            default=Value(0),
 
        )
 

	
 
        products = inventory.Product.objects
 

	
 
        if category:
 
            products = products.filter(category=category)
 

	
 
        products = products.select_related("category")
 
        products = products.annotate(quantity=Sum(quantities_or_zero))
 
        products = products.filter(quantity__gt=0)
 

	
 
        out = []
 
        for prod in products:
 
            out.append(ProductAndQuantity(prod, prod.quantity))
 
        return out
 

	
 
    def items_pending_or_purchased(self):
 
        ''' Returns the items that this user has purchased or has pending. '''
 
        status = [commerce.Cart.STATUS_PAID, commerce.Cart.STATUS_ACTIVE]
 
        return self._items(status)
 

	
 
    def items_purchased(self, category=None):
 
        ''' Aggregates the items that this user has purchased.
 

	
 
        Arguments:
 
            category (Optional[models.inventory.Category]): the category
 
                of items to restrict to.
 

	
 
        Returns:
 
            [ProductAndQuantity, ...]: A list of product-quantity pairs,
 
                aggregating like products from across multiple invoices.
 

	
 
        '''
 
        return self._items(commerce.Cart.STATUS_PAID, category=category)
 

	
 
    def items_pending(self):
 
        ''' Gets all of the items that the user has reserved, but has not yet
vendor/registrasion/registrasion/models/commerce.py
Show inline comments
 
from . import conditions
 
from . import inventory
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from django.core.exceptions import ValidationError
 
from django.db import models
 
from django.db.models import F, Q, Sum
 
from django.utils import timezone
 
from django.utils.encoding import python_2_unicode_compatible
 
from django.utils.translation import ugettext_lazy as _
 
from model_utils.managers import InheritanceManager
 

	
 
User = get_user_model()
 

	
 

	
 
# Commerce Models
 

	
 
@python_2_unicode_compatible
 
class Cart(models.Model):
 
    ''' Represents a set of product items that have been purchased, or are
 
    pending purchase. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        index_together = [
 
            ("status", "time_last_updated"),
 
            ("status", "user"),
 
        ]
 

	
 
    def __str__(self):
 
        return "%d rev #%d" % (self.id, self.revision)
 

	
 
    STATUS_ACTIVE = 1
 
    STATUS_PAID = 2
 
    STATUS_RELEASED = 3
 

	
 
    STATUS_TYPES = [
 
        (STATUS_ACTIVE, _("Active")),
 
        (STATUS_PAID, _("Paid")),
 
        (STATUS_RELEASED, _("Released")),
 
    ]
 

	
 
    user = models.ForeignKey(User)
 
    user = models.ForeignKey(User, on_delete=models.CASCADE)
 
    # ProductItems (foreign key)
 
    vouchers = models.ManyToManyField(inventory.Voucher, blank=True)
 
    time_last_updated = models.DateTimeField(
 
        db_index=True,
 
    )
 
    reservation_duration = models.DurationField()
 
    revision = models.PositiveIntegerField(default=1)
 
    status = models.IntegerField(
 
        choices=STATUS_TYPES,
 
        db_index=True,
 
        default=STATUS_ACTIVE,
 
    )
 

	
 
    @classmethod
 
    def reserved_carts(cls):
 
        ''' Gets all carts that are 'reserved' '''
 
        return Cart.objects.filter(
 
            (Q(status=Cart.STATUS_ACTIVE) &
 
                Q(time_last_updated__gt=(
 
                    timezone.now()-F('reservation_duration')
 
                                        ))) |
 
            Q(status=Cart.STATUS_PAID)
 
        )
 

	
 

	
 
@python_2_unicode_compatible
 
class ProductItem(models.Model):
 
    ''' Represents a product-quantity pair in a Cart. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        ordering = ("product", )
 

	
 
    def __str__(self):
 
        return "product: %s * %d in Cart: %s" % (
 
            self.product, self.quantity, self.cart)
 

	
 
    cart = models.ForeignKey(Cart)
 
    product = models.ForeignKey(inventory.Product)
 
    cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
 
    product = models.ForeignKey(inventory.Product, on_delete=models.CASCADE)
 
    quantity = models.PositiveIntegerField(db_index=True)
 

	
 

	
 
@python_2_unicode_compatible
 
class DiscountItem(models.Model):
 
    ''' Represents a discount-product-quantity relation in a Cart. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        ordering = ("product", )
 

	
 
    def __str__(self):
 
        return "%s: %s * %d in Cart: %s" % (
 
            self.discount, self.product, self.quantity, self.cart)
 

	
 
    cart = models.ForeignKey(Cart)
 
    product = models.ForeignKey(inventory.Product)
 
    discount = models.ForeignKey(conditions.DiscountBase)
 
    cart = models.ForeignKey(Cart, on_delete=models.CASCADE)
 
    product = models.ForeignKey(inventory.Product, on_delete=models.CASCADE)
 
    discount = models.ForeignKey(conditions.DiscountBase,
 
            on_delete=models.CASCADE)
 
    quantity = models.PositiveIntegerField()
 

	
 

	
 
@python_2_unicode_compatible
 
class Invoice(models.Model):
 
    ''' An invoice. Invoices can be automatically generated when checking out
 
    a Cart, in which case, it is attached to a given revision of a Cart.
 

	
 
    Attributes:
 

	
 
        user (User): The owner of this invoice.
 

	
 
        cart (commerce.Cart): The cart that was used to generate this invoice.
 

	
 
        cart_revision (int): The value of ``cart.revision`` at the time of this
 
            invoice's creation. If a change is made to the underlying cart,
 
            this invoice is automatically void -- this change is detected
 
            when ``cart.revision != cart_revision``.
 

	
 
        status (int): One of ``STATUS_UNPAID``, ``STATUS_PAID``,
 
            ``STATUS_REFUNDED``, OR ``STATUS_VOID``. Call
 
            ``get_status_display`` for a human-readable representation.
 

	
 
        recipient (str): A rendered representation of the invoice's recipient.
 

	
 
        issue_time (datetime): When the invoice was issued.
 

	
 
        due_time (datetime): When the invoice is due.
 

	
 
        value (Decimal): The total value of the line items attached to the
 
            invoice.
 

	
 
        lineitem_set (Queryset[LineItem]): The set of line items that comprise
 
            this invoice.
 

	
 
        paymentbase_set(Queryset[PaymentBase]): The set of PaymentBase objects
 
            that have been applied to this invoice.
 

	
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    STATUS_UNPAID = 1
 
    STATUS_PAID = 2
 
    STATUS_REFUNDED = 3
 
    STATUS_VOID = 4
 

	
 
    STATUS_TYPES = [
 
        (STATUS_UNPAID, _("Unpaid")),
 
        (STATUS_PAID, _("Paid")),
 
        (STATUS_REFUNDED, _("Refunded")),
 
        (STATUS_VOID, _("VOID")),
 
    ]
 

	
 
    def __str__(self):
 
        return "Invoice #%d (to: %s, due: %s, value: %s)" % (
 
            self.id, self.user.email, self.due_time, self.value
 
        )
 

	
 
    def clean(self):
 
        if self.cart is not None and self.cart_revision is None:
 
            raise ValidationError(
 
                "If this is a cart invoice, it must have a revision")
 

	
 
    @property
 
    def is_unpaid(self):
 
        return self.status == self.STATUS_UNPAID
 

	
 
    @property
 
    def is_void(self):
 
        return self.status == self.STATUS_VOID
 

	
 
    @property
 
    def is_paid(self):
 
        return self.status == self.STATUS_PAID
 

	
 
    @property
 
    def is_refunded(self):
 
        return self.status == self.STATUS_REFUNDED
 

	
 
    def total_payments(self):
 
        ''' Returns the total amount paid towards this invoice. '''
 

	
 
        payments = PaymentBase.objects.filter(invoice=self)
 
        total_paid = payments.aggregate(Sum("amount"))["amount__sum"] or 0
 
        return total_paid
 

	
 
    def balance_due(self):
 
        ''' Returns the total balance remaining towards this invoice. '''
 
        return self.value - self.total_payments()
 

	
 
    # Invoice Number
 
    user = models.ForeignKey(User)
 
    cart = models.ForeignKey(Cart, null=True)
 
    user = models.ForeignKey(User, on_delete=models.CASCADE)
 
    cart = models.ForeignKey(Cart, null=True, on_delete=models.CASCADE)
 
    cart_revision = models.IntegerField(
 
        null=True,
 
        db_index=True,
 
    )
 
    # Line Items (foreign key)
 
    status = models.IntegerField(
 
        choices=STATUS_TYPES,
 
        db_index=True,
 
    )
 
    recipient = models.CharField(max_length=1024)
 
    issue_time = models.DateTimeField()
 
    due_time = models.DateTimeField()
 
    value = models.DecimalField(max_digits=8, decimal_places=2)
 

	
 

	
 
@python_2_unicode_compatible
 
class LineItem(models.Model):
 
    ''' Line items for an invoice. These are denormalised from the ProductItems
 
    and DiscountItems that belong to a cart (for consistency), but also allow
 
    for arbitrary line items when required.
 

	
 
    Attributes:
 

	
 
        invoice (commerce.Invoice): The invoice to which this LineItem is
 
            attached.
 

	
 
        description (str): A human-readable description of the line item.
 

	
 
        quantity (int): The quantity of items represented by this line.
 

	
 
        price (Decimal): The per-unit price for this line item.
 

	
 
        product (Optional[inventory.Product]): The product that this LineItem
 
            applies to. This allows you to do reports on sales and applied
 
            discounts to individual products.
 

	
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        ordering = ("id", )
 

	
 
    def __str__(self):
 
        return "Line: %s * %d @ %s" % (
 
            self.description, self.quantity, self.price)
 

	
 
    @property
 
    def total_price(self):
 
        ''' price * quantity '''
 
        return self.price * self.quantity
 

	
 
    invoice = models.ForeignKey(Invoice)
 
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
 
    description = models.CharField(max_length=255)
 
    quantity = models.PositiveIntegerField()
 
    price = models.DecimalField(max_digits=8, decimal_places=2)
 
    product = models.ForeignKey(inventory.Product, null=True, blank=True)
 
    product = models.ForeignKey(inventory.Product, null=True, blank=True,
 
            on_delete=models.CASCADE)
 

	
 

	
 
@python_2_unicode_compatible
 
class PaymentBase(models.Model):
 
    ''' The base payment type for invoices. Payment apps should subclass this
 
    class to handle implementation-specific issues.
 

	
 
    Attributes:
 
        invoice (commerce.Invoice): The invoice that this payment applies to.
 

	
 
        time (datetime): The time that this payment was generated. Note that
 
            this will default to the current time when the model is created.
 

	
 
        reference (str): A human-readable reference for the payment, this will
 
            be displayed alongside the invoice.
 

	
 
        amount (Decimal): The amount the payment is for.
 

	
 
    '''
 

	
 
    class Meta:
 
        ordering = ("time", )
 

	
 
    objects = InheritanceManager()
 

	
 
    def __str__(self):
 
        return "Payment: ref=%s amount=%s" % (self.reference, self.amount)
 

	
 
    invoice = models.ForeignKey(Invoice)
 
    invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE)
 
    time = models.DateTimeField(default=timezone.now)
 
    reference = models.CharField(max_length=255)
 
    amount = models.DecimalField(max_digits=8, decimal_places=2)
 

	
 

	
 
class ManualPayment(PaymentBase):
 
    ''' Payments that are manually entered by staff. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    entered_by = models.ForeignKey(User)
 
    entered_by = models.ForeignKey(User, on_delete=models.CASCADE)
 

	
 

	
 
class CreditNote(PaymentBase):
 
    ''' Credit notes represent money accounted for in the system that do not
 
    belong to specific invoices. They may be paid into other invoices, or
 
    cashed out as refunds.
 

	
 
    Each CreditNote may either be used to pay towards another Invoice in the
 
    system (by attaching a CreditNoteApplication), or may be marked as
 
    refunded (by attaching a CreditNoteRefund).'''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    @classmethod
 
    def unclaimed(cls):
 
        return cls.objects.filter(
 
            creditnoteapplication=None,
 
            creditnoterefund=None,
 
        )
 

	
 
    @classmethod
 
    def refunded(cls):
 
        return cls.objects.exclude(creditnoterefund=None)
 

	
 
    @property
 
    def status(self):
 
        if self.is_unclaimed:
 
            return "Unclaimed"
 

	
 
        if hasattr(self, 'creditnoteapplication'):
 
            destination = self.creditnoteapplication.invoice.id
 
            return "Applied to invoice %d" % destination
 

	
 
        elif hasattr(self, 'creditnoterefund'):
 
            reference = self.creditnoterefund.reference
 
            return "Refunded with reference: %s" % reference
 

	
 
        raise ValueError("This should never happen.")
 

	
 
    @property
 
    def is_unclaimed(self):
 
        return not (
 
            hasattr(self, 'creditnoterefund') or
 
            hasattr(self, 'creditnoteapplication')
 
        )
 

	
 
    @property
 
    def value(self):
 
        ''' Returns the value of the credit note. Because CreditNotes are
 
        implemented as PaymentBase objects internally, the amount is a
 
        negative payment against an invoice. '''
 
        return -self.amount
 

	
 

	
 
class CleanOnSave(object):
 

	
 
    def save(self, *a, **k):
 
        self.full_clean()
 
        super(CleanOnSave, self).save(*a, **k)
 

	
 

	
 
class CreditNoteApplication(CleanOnSave, PaymentBase):
 
    ''' Represents an application of a credit note to an Invoice. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    def clean(self):
 
        if not hasattr(self, "parent"):
 
            return
 
        if hasattr(self.parent, 'creditnoterefund'):
 
            raise ValidationError(
 
                "Cannot apply a refunded credit note to an invoice"
 
            )
 

	
 
    parent = models.OneToOneField(CreditNote)
 
    parent = models.OneToOneField(CreditNote, on_delete=models.CASCADE)
 

	
 

	
 
class CreditNoteRefund(CleanOnSave, models.Model):
 
    ''' Represents a refund of a credit note to an external payment.
 
    Credit notes may only be refunded in full. How those refunds are handled
 
    is left as an exercise to the payment app.
 

	
 
    Attributes:
 
        parent (commerce.CreditNote): The CreditNote that this refund
 
            corresponds to.
 

	
 
        time (datetime): The time that this refund was generated.
 

	
 
        reference (str): A human-readable reference for the refund, this should
 
            allow the user to identify the refund in their records.
 

	
 
    '''
 

	
 
    def clean(self):
 
        if not hasattr(self, "parent"):
 
            return
 
        if hasattr(self.parent, 'creditnoteapplication'):
 
            raise ValidationError(
 
                "Cannot refund a credit note that has been paid to an invoice"
 
            )
 

	
 
    parent = models.OneToOneField(CreditNote)
 
    parent = models.OneToOneField(CreditNote, on_delete=models.CASCADE)
 
    time = models.DateTimeField(default=timezone.now)
 
    reference = models.CharField(max_length=255)
 

	
 

	
 
class ManualCreditNoteRefund(CreditNoteRefund):
 
    ''' Credit notes that are entered by a staff member. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    entered_by = models.ForeignKey(User)
 
    entered_by = models.ForeignKey(User, on_delete=models.CASCADE)
vendor/registrasion/registrasion/models/conditions.py
Show inline comments
 
import itertools
 

	
 
from . import inventory
 

	
 
from django.contrib.auth.models import Group
 
from django.core.exceptions import ValidationError
 
from django.db import models
 
from django.db.models import F, Q
 
from django.utils.encoding import python_2_unicode_compatible
 
from django.utils.translation import ugettext_lazy as _
 
from model_utils.managers import InheritanceManager
 

	
 
from symposion import proposals
 

	
 

	
 
# Condition Types
 

	
 
class TimeOrStockLimitCondition(models.Model):
 
    ''' Attributes for a condition that is limited by timespan or a count of
 
    purchased or reserved items.
 

	
 
    Attributes:
 
        start_time (Optional[datetime]): When the condition should start being
 
            true.
 

	
 
        end_time (Optional[datetime]): When the condition should stop being
 
            true.
 

	
 
        limit (Optional[int]): How many items may fall under the condition
 
            the condition until it stops being false -- for all users.
 
    '''
 

	
 
    class Meta:
 
        abstract = True
 

	
 
    start_time = models.DateTimeField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Start time"),
 
        help_text=_("When the condition should start being true"),
 
    )
 
    end_time = models.DateTimeField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("End time"),
 
        help_text=_("When the condition should stop being true."),
 
    )
 
    limit = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit"),
 
        help_text=_(
 
            "How many times this condition may be applied for all users."
 
        ),
 
    )
 

	
 

	
 
class VoucherCondition(models.Model):
 
    ''' A condition is met when a voucher code is in the current cart. '''
 

	
 
    class Meta:
 
        abstract = True
 

	
 
    voucher = models.OneToOneField(
 
        inventory.Voucher,
 
        on_delete=models.CASCADE,
 
        verbose_name=_("Voucher"),
 
        db_index=True,
 
    )
 

	
 

	
 
class IncludedProductCondition(models.Model):
 
    class Meta:
 
        abstract = True
 

	
 
    enabling_products = models.ManyToManyField(
 
        inventory.Product,
 
        verbose_name=_("Including product"),
 
        help_text=_("If one of these products are purchased, this condition "
 
                    "is met."),
 
    )
 

	
 

	
 
class SpeakerCondition(models.Model):
 
    ''' Conditions that are met if a user is a presenter, or copresenter,
 
    of a specific kind of presentation. '''
 

	
 
    class Meta:
 
        abstract = True
 

	
 
    is_presenter = models.BooleanField(
 
        blank=True,
 
        help_text=_("This condition is met if the user is the primary "
 
                    "presenter of a presentation."),
 
    )
 
    is_copresenter = models.BooleanField(
 
        blank=True,
 
        help_text=_("This condition is met if the user is a copresenter of a "
 
                    "presentation."),
 
    )
 
    proposal_kind = models.ManyToManyField(
 
        proposals.models.ProposalKind,
 
        "symposion_proposals.ProposalKind",
 
        help_text=_("The types of proposals that these users may be "
 
                    "presenters of."),
 
    )
 

	
 

	
 
class GroupMemberCondition(models.Model):
 
    ''' Conditions that are met if a user is a member (not declined or
 
    rejected) of a specific django auth group. '''
 

	
 
    class Meta:
 
        abstract = True
 

	
 
    group = models.ManyToManyField(
 
        Group,
 
        help_text=_("The groups a user needs to be a member of for this"
 
                    "condition to be met."),
 
    )
 

	
 

	
 
# Discounts
 

	
 
@python_2_unicode_compatible
 
class DiscountBase(models.Model):
 
    ''' Base class for discounts. This class is subclassed with special
 
    attributes which are used to determine whether or not the given discount
 
    is available to be added to the current cart.
 

	
 
    Attributes:
 
        description (str): Display text that appears on the attendee's Invoice
 
            when the discount is applied to a Product on that invoice.
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    objects = InheritanceManager()
 

	
 
    def __str__(self):
 
        return "Discount: " + self.description
 

	
 
    def effects(self):
 
        ''' Returns all of the effects of this discount. '''
 
        products = self.discountforproduct_set.all()
 
        categories = self.discountforcategory_set.all()
 
        return itertools.chain(products, categories)
 

	
 
    description = models.CharField(
 
        max_length=255,
 
        verbose_name=_("Description"),
 
        help_text=_("A description of this discount. This will be included on "
 
                    "invoices where this discount is applied."),
 
        )
 

	
 

	
 
@python_2_unicode_compatible
 
class DiscountForProduct(models.Model):
 
    ''' Represents a discount on an individual product. Each Discount can
 
    contain multiple products and categories. Discounts can either be a
 
    percentage or a fixed amount, but not both.
 

	
 
    Attributes:
 
        product (inventory.Product): The product that this discount line will
 
            apply to.
 

	
 
        percentage (Decimal): The percentage discount that will be *taken off*
 
            this product if this discount applies.
 

	
 
        price (Decimal): The currency value that will be *taken off* this
 
            product if this discount applies.
 

	
 
        quantity (int): The number of times that each user may apply this
 
            discount line. This applies across every valid Invoice that
 
            the user has.
 

	
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    def __str__(self):
 
        if self.percentage:
 
            return "%s%% off %s" % (self.percentage, self.product)
 
        elif self.price:
 
            return "$%s off %s" % (self.price, self.product)
 

	
 
    def clean(self):
 
        if self.percentage is None and self.price is None:
 
            raise ValidationError(
 
                _("Discount must have a percentage or a price."))
 
        elif self.percentage is not None and self.price is not None:
 
            raise ValidationError(
 
                _("Discount may only have a percentage or only a price."))
 

	
 
        prods = DiscountForProduct.objects.filter(
 
            discount=self.discount,
 
            product=self.product)
...
 
@@ -427,150 +425,151 @@ class FlagBase(models.Model):
 
                    " conditions must be met. If there is at least one "
 
                    "'enable if true' flag, at least one such condition must "
 
                    "be met. If both types of conditions exist on a product, "
 
                    "both of these rules apply."
 
                    ),
 
    )
 
    products = models.ManyToManyField(
 
        inventory.Product,
 
        blank=True,
 
        help_text=_("Products affected by this flag's condition."),
 
        related_name="flagbase_set",
 
    )
 
    categories = models.ManyToManyField(
 
        inventory.Category,
 
        blank=True,
 
        help_text=_("Categories whose products are affected by this flag's "
 
                    "condition."
 
                    ),
 
        related_name="flagbase_set",
 
    )
 

	
 
    @property
 
    def all_products(self):
 
        all_products = inventory.Product.objects.all()
 
        all_products = all_products.filter(
 
            (
 
                Q(id__in=self.products.all()) |
 
                Q(category_id__in=self.categories.all())
 
            )
 
        )
 
        return all_products
 

	
 

	
 
class TimeOrStockLimitFlag(TimeOrStockLimitCondition, FlagBase):
 
    ''' Product groupings that can be used to enable a product during a
 
    specific date range, or when fewer than a limit of products have been
 
    sold.
 

	
 
    Attributes:
 
        start_time (Optional[datetime]): This condition is only met after this
 
            time.
 

	
 
        end_time (Optional[datetime]): This condition is only met before this
 
            time.
 

	
 
        limit (Optional[int]): The number of products that *all users* can
 
            purchase under this limit, regardless of their per-user limits.
 

	
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        verbose_name = _("flag (time/stock limit)")
 
        verbose_name_plural = _("flags (time/stock limit)")
 

	
 

	
 
@python_2_unicode_compatible
 
class ProductFlag(IncludedProductCondition, FlagBase):
 
    ''' The condition is met because a specific product is purchased.
 

	
 
    Attributes:
 
        enabling_products ([inventory.Product, ...]): The products that cause
 
            this condition to be met.
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        verbose_name = _("flag (dependency on product)")
 
        verbose_name_plural = _("flags (dependency on product)")
 

	
 
    def __str__(self):
 
        return "Enabled by products: " + str(self.enabling_products.all())
 

	
 

	
 
@python_2_unicode_compatible
 
class CategoryFlag(FlagBase):
 
    ''' The condition is met because a product in a particular product is
 
    purchased.
 

	
 
    Attributes:
 
        enabling_category (inventory.Category): The category that causes this
 
            condition to be met.
 
     '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        verbose_name = _("flag (dependency on product from category)")
 
        verbose_name_plural = _("flags (dependency on product from category)")
 

	
 
    def __str__(self):
 
        return "Enabled by product in category: " + str(self.enabling_category)
 

	
 
    enabling_category = models.ForeignKey(
 
        inventory.Category,
 
        help_text=_("If a product from this category is purchased, this "
 
                    "condition is met."),
 
        on_delete=models.CASCADE,
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class VoucherFlag(VoucherCondition, FlagBase):
 
    ''' The condition is met because a Voucher is present. This is for e.g.
 
    enabling sponsor tickets. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        verbose_name = _("flag (dependency on voucher)")
 
        verbose_name_plural = _("flags (dependency on voucher)")
 

	
 
    def __str__(self):
 
        return "Enabled by voucher: %s" % self.voucher
 

	
 

	
 
class SpeakerFlag(SpeakerCondition, FlagBase):
 
    ''' Conditions that are enabled because the user is a presenter or
 
    co-presenter of a kind of presentation.
 

	
 
    Attributes:
 
        is_presenter (bool): The condition should be met if the user is a
 
            presenter of a presentation.
 

	
 
        is_copresenter (bool): The condition should be met if the user is a
 
            copresenter of a presentation.
 

	
 
        proposal_kind ([symposion.proposals.models.ProposalKind, ...]): The
 
            kinds of proposals that the user may be a presenter or
 
            copresenter of for this condition to be met.
 

	
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        verbose_name = _("flag (speaker)")
 
        verbose_name_plural = _("flags (speaker)")
 

	
 

	
 
class GroupMemberFlag(GroupMemberCondition, FlagBase):
 
    ''' Flag whose conditions are metbecause the user is a member of a specific
 
    django auth Group.
 

	
 
    Attributes:
 
        group ([Group, ...]): The condition should be met if the user is a
 
            member of one of these groups.
 

	
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        verbose_name = _("flag (group member)")
 
        verbose_name_plural = _("flags (group member)")
vendor/registrasion/registrasion/models/inventory.py
Show inline comments
...
 
@@ -63,161 +63,162 @@ class Category(models.Model):
 
        return self.name
 

	
 
    RENDER_TYPE_RADIO = 1
 
    RENDER_TYPE_QUANTITY = 2
 
    RENDER_TYPE_ITEM_QUANTITY = 3
 
    RENDER_TYPE_CHECKBOX = 4
 

	
 
    CATEGORY_RENDER_TYPES = [
 
        (RENDER_TYPE_RADIO, _("Radio button")),
 
        (RENDER_TYPE_QUANTITY, _("Quantity boxes")),
 
        (RENDER_TYPE_ITEM_QUANTITY, _("Product selector and quantity box")),
 
        (RENDER_TYPE_CHECKBOX, _("Checkbox button")),
 
    ]
 

	
 
    name = models.CharField(
 
        max_length=255,
 
        verbose_name=_("Name"),
 
    )
 
    description = models.TextField(
 
        verbose_name=_("Description"),
 
    )
 
    limit_per_user = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit per user"),
 
        help_text=_("The total number of items from this category one "
 
                    "attendee may purchase."),
 
    )
 
    required = models.BooleanField(
 
        blank=True,
 
        help_text=_("If enabled, a user must select an "
 
                    "item from this category."),
 
    )
 
    order = models.PositiveIntegerField(
 
        verbose_name=("Display order"),
 
        db_index=True,
 
    )
 
    render_type = models.IntegerField(
 
        choices=CATEGORY_RENDER_TYPES,
 
        verbose_name=_("Render type"),
 
        help_text=_("The registration form will render this category in this "
 
                    "style."),
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class Product(models.Model):
 
    ''' Products make up the conference inventory.
 

	
 
    Attributes:
 
        name (str): The display name for the product.
 

	
 
        description (str): Some descriptive text that will help the user to
 
            understand the product when they're at the registration form.
 

	
 
        category (Category): The Category that this product will be grouped
 
            under.
 

	
 
        price (Decimal): The price that 1 unit of this product will sell for.
 
            Note that this should be the full price, before any discounts are
 
            applied.
 

	
 
        limit_per_user (Optional[int]): This restricts the number of this
 
            Product that each attendee may claim. This extends across multiple
 
            Invoices.
 

	
 
        reservation_duration (datetime): When a Product is added to the user's
 
            tentative registration, it is marked as unavailable for a period of
 
            time. This allows the user to build up their registration and then
 
            pay for it. This reservation duration determines how long an item
 
            should be allowed to be reserved whilst being unpaid.
 

	
 
        order (int): An ascending order for displaying the Products
 
            within each Category.
 

	
 
    '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 
        verbose_name = _("inventory - product")
 
        ordering = ("category__order", "order")
 

	
 
    def __str__(self):
 
        return "%s - %s" % (self.category.name, self.name)
 

	
 
    name = models.CharField(
 
        max_length=255,
 
        verbose_name=_("Name"),
 
    )
 
    description = models.TextField(
 
        verbose_name=_("Description"),
 
        null=True,
 
        blank=True,
 
    )
 
    category = models.ForeignKey(
 
        Category,
 
        verbose_name=_("Product category")
 
        verbose_name=_("Product category"),
 
        on_delete=models.CASCADE,
 
    )
 
    price = models.DecimalField(
 
        max_digits=8,
 
        decimal_places=2,
 
        verbose_name=_("Price"),
 
    )
 
    limit_per_user = models.PositiveIntegerField(
 
        null=True,
 
        blank=True,
 
        verbose_name=_("Limit per user"),
 
    )
 
    reservation_duration = models.DurationField(
 
        default=datetime.timedelta(hours=1),
 
        verbose_name=_("Reservation duration"),
 
        help_text=_("The length of time this product will be reserved before "
 
                    "it is released for someone else to purchase."),
 
    )
 
    order = models.PositiveIntegerField(
 
        verbose_name=("Display order"),
 
        db_index=True,
 
    )
 

	
 

	
 
@python_2_unicode_compatible
 
class Voucher(models.Model):
 
    ''' Vouchers are used to enable Discounts or Flags for the people who hold
 
    the voucher code.
 

	
 
    Attributes:
 
        recipient (str): A display string used to identify the holder of the
 
            voucher on the admin page.
 

	
 
        code (str): The string that is used to prove that the attendee holds
 
            this voucher.
 

	
 
        limit (int): The number of attendees who are permitted to hold this
 
            voucher.
 

	
 
     '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    # Vouchers reserve a cart for a fixed amount of time, so that
 
    # items may be added without the voucher being swiped by someone else
 
    RESERVATION_DURATION = datetime.timedelta(hours=1)
 

	
 
    def __str__(self):
 
        return "Voucher for %s" % self.recipient
 

	
 
    @classmethod
 
    def normalise_code(cls, code):
 
        return code.upper()
 

	
 
    def save(self, *a, **k):
 
        ''' Normalise the voucher code to be uppercase '''
 
        self.code = self.normalise_code(self.code)
 
        super(Voucher, self).save(*a, **k)
 

	
 
    recipient = models.CharField(max_length=64, verbose_name=_("Recipient"))
 
    code = models.CharField(max_length=16,
 
                            unique=True,
 
                            verbose_name=_("Voucher code"))
 
    limit = models.PositiveIntegerField(verbose_name=_("Voucher use limit"))
vendor/registrasion/registrasion/models/people.py
Show inline comments
 
from registrasion import util
 

	
 
from django.conf import settings
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.db import models
 
from django.utils.encoding import python_2_unicode_compatible
 
from model_utils.managers import InheritanceManager
 

	
 
from registrasion.models.commerce import Invoice, ProductItem
 

	
 
User = get_user_model()
 

	
 

	
 
# User models
 

	
 
@python_2_unicode_compatible
 
class Attendee(models.Model):
 
    ''' Miscellaneous user-related data. '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    def __str__(self):
 
        return "%s" % self.user
 

	
 
    @staticmethod
 
    def get_instance(user):
 
        ''' Returns the instance of attendee for the given user, or creates
 
        a new one. '''
 
        try:
 
            return Attendee.objects.get(user=user)
 
        except ObjectDoesNotExist:
 
            return Attendee.objects.create(user=user)
 

	
 
    def save(self, *a, **k):
 
        while not self.access_code:
 
            access_code = util.generate_access_code()
 
            if Attendee.objects.filter(access_code=access_code).count() == 0:
 
                self.access_code = access_code
 
        return super(Attendee, self).save(*a, **k)
 

	
 
    user = models.OneToOneField(User, on_delete=models.CASCADE)
 
    # Badge/profile is linked
 
    access_code = models.CharField(
 
        max_length=6,
 
        unique=True,
 
        db_index=True,
 
    )
 
    completed_registration = models.BooleanField(default=False)
 
    guided_categories_complete = models.ManyToManyField("category", blank=True)
 

	
 
    @property
 
    def ticket_type(self):
 
        tickets = ProductItem.objects.select_related(
 
            "product","product__category","cart",
 
        )
 
        tickets = tickets.filter(
 
            product__category=settings.TICKET_PRODUCT_CATEGORY,
 
            cart__invoice__status=Invoice.STATUS_PAID,
 
            cart__invoice__user=self.user
 
        )
 
        if len(tickets) >1:
 
            raise ValueError("Too many tickets for attendee %s", self)
 
        return tickets[0].product.name
 

	
 
class AttendeeProfileBase(models.Model):
 
    ''' Information for an attendee's badge and related preferences.
 
    Subclass this in your Django site to ask for attendee information in your
 
    registration progess.
 
     '''
 

	
 
    class Meta:
 
        app_label = "registrasion"
 

	
 
    objects = InheritanceManager()
 

	
 
    @classmethod
 
    def name_field(cls):
 
        '''
 
        Returns:
 
            The name of a field that stores the attendee's name. This is used
 
            to pre-fill the attendee's name from their Speaker profile, if they
 
            have one.
 
        '''
 
        return None
 

	
 
    def attendee_name(self):
 
        if type(self) == AttendeeProfileBase:
 
            real = AttendeeProfileBase.objects.get_subclass(id=self.id)
 
        else:
 
            real = self
 
        return getattr(real, real.name_field())
 

	
 
    def invoice_recipient(self):
 
        '''
 

	
 
        Returns:
 
            A representation of this attendee profile for the purpose
 
            of rendering to an invoice. This should include any information
 
            that you'd usually include on an invoice. Override in subclasses.
 
        '''
 

	
 
        # Manual dispatch to subclass. Fleh.
 
        slf = AttendeeProfileBase.objects.get_subclass(id=self.id)
 
        # Actually compare the functions.
 
        if type(slf).invoice_recipient != type(self).invoice_recipient:
 
            return type(slf).invoice_recipient(slf)
 

	
 
        # Return a default
vendor/registrasion/registrasion/reporting/reports.py
Show inline comments
 
import csv
 

	
 
from django.contrib.auth.decorators import user_passes_test
 
from django.shortcuts import render
 
from django.core.urlresolvers import reverse
 
from django.http import HttpResponse
 
from django.urls import reverse
 
from functools import wraps
 

	
 
from registrasion import views
 

	
 

	
 
''' A list of report views objects that can be used to load a list of
 
reports. '''
 
_all_report_views = []
 

	
 

	
 
class Report(object):
 

	
 
    def __init__(self):
 
        pass
 

	
 
    def title():
 
        raise NotImplementedError
 

	
 
    def headings():
 
        ''' Returns the headings for the report. '''
 
        raise NotImplementedError
 

	
 
    def rows(content_type):
 
        '''
 

	
 
        Arguments:
 
            content_type (str): The content-type for the output format of this
 
            report.
 

	
 
        Returns:
 
            An iterator, which yields each row of the data. Each row should
 
            be an iterable containing the cells, rendered appropriately for
 
            content_type.
 
        '''
 
        raise NotImplementedError
 

	
 
    def _linked_text(self, content_type, address, text):
 
        '''
 

	
 
        Returns:
 
            an HTML linked version of text, if the content_type for this report
 
            is HTMLish, otherwise, the text.
 
        '''
 

	
 
        if content_type == "text/html":
 
            return Report._html_link(address, text)
 
        else:
 
            return text
 

	
 
    @staticmethod
 
    def _html_link(address, text):
 
        return '<a href="%s">%s</a>' % (address, text)
 

	
 

	
 
class _ReportTemplateWrapper(object):
 
    ''' Used internally to pass `Report` objects to templates. They effectively
 
    are used to specify the content_type for a report. '''
 

	
 
    def __init__(self, content_type, report):
 
        self.content_type = content_type
 
        self.report = report
 

	
 
    def title(self):
 
        return self.report.title()
 

	
 
    def headings(self):
 
        return self.report.headings()
 

	
 
    def rows(self):
 
        return self.report.rows(self.content_type)
 

	
 
    def count(self):
 
        return self.report.count()
 

	
 

	
 
class BasicReport(Report):
 

	
 
    def __init__(self, title, headings, link_view=None):
 
        super(BasicReport, self).__init__()
 
        self._title = title
 
        self._headings = headings
 
        self._link_view = link_view
 

	
 
    def title(self):
 
        ''' Returns the title for this report. '''
 
        return self._title
 

	
 
    def headings(self):
 
        ''' Returns the headings for the table. '''
 
        return self._headings
 

	
 
    def cell_text(self, content_type, index, text):
 
        if index > 0 or not self._link_view:
 
            return text
 
        else:
 
            address = self.get_link(text)
vendor/registrasion/registrasion/reporting/views.py
Show inline comments
 
from . import forms
 

	
 
import collections
 
import datetime
 
import itertools
 

	
 
from django.conf import settings
 
from django.contrib.auth.decorators import user_passes_test
 
from django.contrib.auth.models import User
 
from django.core.urlresolvers import reverse
 
from django.contrib.auth import get_user_model
 
from django.db import models
 
from django.db.models import F, Q, Subquery, OuterRef
 
from django.db.models import Count, Max, Sum
 
from django.db.models import Case, When, Value
 
from django.db.models.fields.related import RelatedField
 
from django.db.models.fields import CharField
 
from django.shortcuts import render
 
from django.urls import reverse
 

	
 
from registrasion.controllers.cart import CartController
 
from registrasion.controllers.item import ItemController
 
from registrasion.models import conditions
 
from registrasion.models import commerce
 
from registrasion.models import people
 
from registrasion import util
 
from registrasion import views
 

	
 
from symposion.schedule import models as schedule_models
 

	
 
from .reports import get_all_reports
 
from .reports import Links
 
from .reports import ListReport
 
from .reports import QuerysetReport
 
from .reports import report_view
 

	
 
import bleach
 

	
 

	
 
def CURRENCY():
 
    return models.DecimalField(decimal_places=2)
 

	
 

	
 
User = get_user_model()
 
AttendeeProfile = util.get_object_from_name(settings.ATTENDEE_PROFILE_MODEL)
 

	
 

	
 
@user_passes_test(views._staff_only)
 
def reports_list(request):
 
    ''' Lists all of the reports currently available. '''
 

	
 
    reports = []
 

	
 
    for report in get_all_reports():
 
        reports.append({
 
            "name": report.__name__,
 
            "title": report.title,
 
            "url": reverse(report),
 
            "description": report.__doc__,
 
        })
 

	
 
    reports.sort(key=lambda report: report["name"])
 

	
 
    ctx = {
 
        "reports": reports,
 
    }
 

	
 
    return render(request, "registrasion/reports_list.html", ctx)
 

	
 

	
 
# Report functions
 

	
 
@report_view("Reconcilitation")
 
def reconciliation(request, form):
 
    ''' Shows the summary of sales, and the full history of payments and
 
    refunds into the system. '''
 

	
 
    return [
 
        sales_payment_summary(),
 
        items_sold(),
 
        payments(),
 
        credit_note_refunds(),
 
    ]
 

	
 

	
 
def items_sold():
 
    ''' Summarises the items sold and discounts granted for a given set of
 
    products, or products from categories. '''
 

	
 
    data = None
 
    headings = None
 

	
 
    line_items = commerce.LineItem.objects.filter(
 
        invoice__status=commerce.Invoice.STATUS_PAID,
 
    ).select_related("invoice")
 

	
 
    line_items = line_items.order_by(
 
        # sqlite requires an order_by for .values() to work
 
        "-price", "description",
 
    ).values(
 
        "price", "description",
 
    ).annotate(
 
        total_quantity=Sum("quantity"),
 
    )
 

	
 
    headings = ["Description", "Quantity", "Price", "Total"]
 

	
 
    data = []
 
    total_income = 0
 
    for line in line_items:
 
        cost = line["total_quantity"] * line["price"]
 
        data.append([
 
            line["description"], line["total_quantity"],
 
            line["price"], cost,
 
        ])
 
        total_income += cost
 

	
 
    data.append([
 
        "(TOTAL)", "--", "--", total_income,
 
    ])
 

	
 
    return ListReport("Items sold", headings, data)
 

	
 

	
 
def sales_payment_summary():
 
    ''' Summarises paid items and payments. '''
 

	
 
    def value_or_zero(aggregate, key):
 
        return aggregate[key] or 0
 

	
 
    def sum_amount(payment_set):
 
        a = payment_set.values("amount").aggregate(total=Sum("amount"))
 
        return value_or_zero(a, "total")
 

	
 
    headings = ["Category", "Total"]
 
    data = []
 

	
 
    # Summarise all sales made (= income.)
 
    sales = commerce.LineItem.objects.filter(
 
        invoice__status=commerce.Invoice.STATUS_PAID,
vendor/registrasion/registrasion/templatetags/registrasion_tags.py
Show inline comments
 
from registrasion.models import commerce
 
from registrasion.controllers.category import CategoryController
 
from registrasion.controllers.item import ItemController
 

	
 
from django import template
 
from django.conf import settings
 
from django.db.models import Sum
 
try:
 
    from urllib import urlencode
 
except ImportError:
 
    from urllib.parse import urlencode
 
from urllib.parse import urlencode
 

	
 
from operator import attrgetter
 

	
 
register = template.Library()
 

	
 

	
 
def user_for_context(context):
 
    ''' Returns either context.user or context.request.user if the former is
 
    not defined. '''
 
    try:
 
        return context["user"]
 
    except KeyError:
 
        return context.request.user
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def available_categories(context):
 
    ''' Gets all of the currently available products.
 

	
 
    Returns:
 
        [models.inventory.Category, ...]: A list of all of the categories that
 
            have Products that the current user can reserve.
 

	
 
    '''
 
    return CategoryController.available_categories(user_for_context(context))
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def missing_categories(context):
 
    ''' Adds the categories that the user does not currently have. '''
 
    user = user_for_context(context)
 
    categories_available = set(CategoryController.available_categories(user))
 
    items = ItemController(user).items_pending_or_purchased()
 

	
 
    categories_held = set()
 

	
 
    for product, quantity in items:
 
        categories_held.add(product.category)
 

	
 
    missing = categories_available - categories_held
 

	
 
    return sorted(set(i for i in missing), key=attrgetter("order"))
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def available_credit(context):
 
    ''' Calculates the sum of unclaimed credit from this user's credit notes.
 

	
 
    Returns:
 
        Decimal: the sum of the values of unclaimed credit notes for the
 
            current user.
 

	
 
    '''
 

	
 
    notes = commerce.CreditNote.unclaimed().filter(
 
        invoice__user=user_for_context(context),
 
    )
 
    ret = notes.values("amount").aggregate(Sum("amount"))["amount__sum"] or 0
 
    return 0 - ret
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def invoices(context):
 
    '''
 

	
 
    Returns:
 
        [models.commerce.Invoice, ...]: All of the current user's invoices. '''
 
    return commerce.Invoice.objects.filter(user=user_for_context(context))
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def items_pending(context):
 
    ''' Gets all of the items that the user from this context has reserved.
 

	
 
    The user will be either `context.user`, and `context.request.user` if
 
    the former is not defined.
 
    '''
 

	
 
    return ItemController(user_for_context(context)).items_pending()
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def items_purchased(context, category=None):
 
    ''' Returns the items purchased for this user.
 

	
 
    The user will be either `context.user`, and `context.request.user` if
 
    the former is not defined.
 
    '''
 

	
 
    return ItemController(user_for_context(context)).items_purchased(
 
        category=category
 
    )
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def total_items_purchased(context, category=None):
 
    ''' Returns the number of items purchased for this user (sum of quantities).
 

	
 
    The user will be either `context.user`, and `context.request.user` if
 
    the former is not defined.
 
    '''
 

	
 
    return sum(i.quantity for i in items_purchased(context, category))
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def report_as_csv(context, section):
 

	
 
    old_query = context.request.META["QUERY_STRING"]
 
    query = dict([("section", section), ("content_type", "text/csv")])
 
    querystring = urlencode(query)
 

	
 
    if old_query:
 
        querystring = old_query + "&" + querystring
 

	
 
    return context.request.path + "?" + querystring
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def sold_out_and_unregistered(context):
 
    ''' If the current user is unregistered, returns True if there are no
 
    products in the TICKET_PRODUCT_CATEGORY that are available to that user.
 

	
 
    If there *are* products available, the return False.
 

	
 
    If the current user *is* registered, then return None (it's not a
 
    pertinent question for people who already have a ticket).
 

	
 
    '''
 

	
 
    user = user_for_context(context)
 
    if hasattr(user, "attendee") and user.attendee.completed_registration:
 
        # This user has completed registration, and so we don't need to answer
 
        # whether they have sold out yet.
 

	
 
        # TODO: what if a user has got to the review phase?
 
        # currently that user will hit the review page, click "Check out and
 
        # pay", and that will fail. Probably good enough for now.
 

	
 
        return None
 

	
 
    ticket_category = settings.TICKET_PRODUCT_CATEGORY
 
    categories = available_categories(context)
 

	
 
    return ticket_category not in [cat.id for cat in categories]
 

	
 

	
 
class IncludeNode(template.Node):
 
    ''' https://djangosnippets.org/snippets/2058/ '''
 

	
 

	
 
    def __init__(self, template_name):
 
        # template_name as passed in includes quotmarks?
 
        # strip them from the start and end
 
        self.template_name = template_name[1:-1]
 

	
 
    def render(self, context):
 
        try:
 
            # Loading the template and rendering it
 
            return template.loader.render_to_string(
 
                self.template_name, context=context,
 
            )
 
        except template.TemplateDoesNotExist:
 
            return ""
 

	
 

	
 
@register.tag
 
def include_if_exists(parser, token):
 
    """Usage: {% include_if_exists "head.html" %}
 

	
 
    This will fail silently if the template doesn't exist. If it does, it will
 
    be rendered with the current context.
 

	
 
    From: https://djangosnippets.org/snippets/2058/
 
    """
 
    try:
 
        tag_name, template_name = token.split_contents()
 
    except ValueError:
 
        raise (template.TemplateSyntaxError,
 
        raise template.TemplateSyntaxError(
 
            "%r tag requires a single argument" % token.contents.split()[0])
 

	
 
    return IncludeNode(template_name)
vendor/registrasion/registrasion/tests/test_batch.py
Show inline comments
...
 
@@ -34,104 +34,104 @@ class BatchTestCase(RegistrationCartTestCase):
 
            cache_3 = BatchController.get_cache(self.USER_1)
 

	
 
        self.assertIs(cache_1, cache_2)
 
        self.assertIs(cache_2, cache_3)
 

	
 
    def test_caches_are_independent_for_different_users(self):
 
        with BatchController.batch(self.USER_1):
 
            cache_1 = BatchController.get_cache(self.USER_1)
 

	
 
            with BatchController.batch(self.USER_2):
 
                cache_2 = BatchController.get_cache(self.USER_2)
 

	
 
        self.assertIsNot(cache_1, cache_2)
 

	
 
    def test_cache_clears_are_independent_for_different_users(self):
 
        with BatchController.batch(self.USER_1):
 
            cache_1 = BatchController.get_cache(self.USER_1)
 

	
 
            with BatchController.batch(self.USER_2):
 
                cache_2 = BatchController.get_cache(self.USER_2)
 

	
 
            with BatchController.batch(self.USER_2):
 
                cache_3 = BatchController.get_cache(self.USER_2)
 

	
 
            cache_4 = BatchController.get_cache(self.USER_1)
 

	
 
        self.assertIs(cache_1, cache_4)
 
        self.assertIsNot(cache_1, cache_2)
 
        self.assertIsNot(cache_2, cache_3)
 

	
 
    def test_new_caches_for_new_batches(self):
 
        with BatchController.batch(self.USER_1):
 
            cache_1 = BatchController.get_cache(self.USER_1)
 

	
 
        with BatchController.batch(self.USER_1):
 
            cache_2 = BatchController.get_cache(self.USER_1)
 

	
 
            with BatchController.batch(self.USER_1):
 
                cache_3 = BatchController.get_cache(self.USER_1)
 

	
 
        self.assertIs(cache_2, cache_3)
 
        self.assertIsNot(cache_1, cache_2)
 

	
 
    def test_memoisation_happens_in_batch_context(self):
 
        with BatchController.batch(self.USER_1):
 
            output_1 = self._memoiseme(self.USER_1)
 

	
 
            with BatchController.batch(self.USER_1):
 
                output_2 = self._memoiseme(self.USER_1)
 

	
 
        self.assertIs(output_1, output_2)
 

	
 
    def test_memoisaion_does_not_happen_outside_batch_context(self):
 
        output_1 = self._memoiseme(self.USER_1)
 
        output_2 = self._memoiseme(self.USER_1)
 

	
 
        self.assertIsNot(output_1, output_2)
 

	
 
    def test_memoisation_is_user_independent(self):
 
        with BatchController.batch(self.USER_1):
 
            output_1 = self._memoiseme(self.USER_1)
 
            with BatchController.batch(self.USER_2):
 
                output_2 = self._memoiseme(self.USER_2)
 
                output_3 = self._memoiseme(self.USER_1)
 

	
 
        self.assertIsNot(output_1, output_2)
 
        self.assertIs(output_1, output_3)
 

	
 
    def test_memoisation_clears_outside_batches(self):
 
        with BatchController.batch(self.USER_1):
 
            output_1 = self._memoiseme(self.USER_1)
 

	
 
        with BatchController.batch(self.USER_1):
 
            output_2 = self._memoiseme(self.USER_1)
 

	
 
        self.assertIsNot(output_1, output_2)
 

	
 
    @classmethod
 
    @BatchController.memoise
 
    def _memoiseme(self, user):
 
        return object()
 

	
 
    def test_batch_end_functionality_is_called(self):
 
        class Ender(object):
 
            end_count = 0
 

	
 
            def end_batch(self):
 
                self.end_count += 1
 

	
 
        @BatchController.memoise
 
        def get_ender(user):
 
            return Ender()
 

	
 
        # end_batch should get called once on exiting the batch
 
        with BatchController.batch(self.USER_1):
 
            ender = get_ender(self.USER_1)
 
        self.assertEquals(1, ender.end_count)
 
        self.assertEqual(1, ender.end_count)
 

	
 
        # end_batch should get called once on exiting the batch
 
        # no matter how deep the object gets cached
 
        with BatchController.batch(self.USER_1):
 
            with BatchController.batch(self.USER_1):
 
                ender = get_ender(self.USER_1)
 
        self.assertEquals(1, ender.end_count)
 
        self.assertEqual(1, ender.end_count)
vendor/registrasion/registrasion/tests/test_cart.py
Show inline comments
 
import datetime
 
import pytz
 

	
 
from decimal import Decimal
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
 
from django.core.management import call_command
 
from django.test import TestCase
 

	
 
from registrasion.models import commerce
 
from registrasion.models import conditions
 
from registrasion.models import inventory
 
from registrasion.models import people
 
from registrasion.controllers.batch import BatchController
 
from registrasion.controllers.product import ProductController
 

	
 
from registrasion.tests.controller_helpers import TestingCartController
 
from registrasion.tests.patches import MixInPatches
 

	
 
UTC = pytz.timezone('UTC')
 
User = get_user_model()
 

	
 

	
 
class RegistrationCartTestCase(MixInPatches, TestCase):
 

	
 
    def setUp(self):
 
        super(RegistrationCartTestCase, self).setUp()
 

	
 
    def tearDown(self):
 
        if True:
 
            # If you're seeing segfaults in tests, enable this.
 
            call_command(
 
                'flush',
 
                verbosity=0,
 
                interactive=False,
 
                reset_sequences=False,
 
                allow_cascade=False,
 
                inhibit_post_migrate=False
 
            )
 

	
 
        super(RegistrationCartTestCase, self).tearDown()
 

	
 
    @classmethod
 
    def setUpTestData(cls):
 

	
 
        super(RegistrationCartTestCase, cls).setUpTestData()
 

	
 
        cls.USER_1 = User.objects.create_user(
 
            username='testuser',
 
            email='test@example.com',
 
            password='top_secret')
 

	
 
        cls.USER_2 = User.objects.create_user(
 
            username='testuser2',
 
            email='test2@example.com',
 
            password='top_secret')
 

	
 
        attendee1 = people.Attendee.get_instance(cls.USER_1)
 
        people.AttendeeProfileBase.objects.create(
 
            attendee=attendee1,
 
        )
 
        attendee2 = people.Attendee.get_instance(cls.USER_2)
 
        people.AttendeeProfileBase.objects.create(
 
            attendee=attendee2,
 
        )
 

	
 
        cls.RESERVATION = datetime.timedelta(hours=1)
 

	
 
        cls.categories = []
 
        for i in range(2):
 
            cat = inventory.Category.objects.create(
 
                name="Category " + str(i + 1),
 
                description="This is a test category",
 
                order=i,
 
                render_type=inventory.Category.RENDER_TYPE_RADIO,
 
                required=False,
 
            )
 
            cls.categories.append(cat)
 

	
 
        cls.CAT_1 = cls.categories[0]
 
        cls.CAT_2 = cls.categories[1]
 

	
 
        cls.products = []
 
        for i in range(4):
 
            prod = inventory.Product.objects.create(
 
                name="Product " + str(i + 1),
 
                description="This is a test product.",
 
                category=cls.categories[i / 2],  # 2 products per category
 
                category=cls.categories[i // 2],  # 2 products per category
 
                price=Decimal("10.00"),
 
                reservation_duration=cls.RESERVATION,
 
                limit_per_user=10,
 
                order=1,
 
            )
 
            cls.products.append(prod)
 

	
 
        cls.PROD_1 = cls.products[0]
 
        cls.PROD_2 = cls.products[1]
 
        cls.PROD_3 = cls.products[2]
 
        cls.PROD_4 = cls.products[3]
 

	
 
        cls.PROD_4.price = Decimal("5.00")
 
        cls.PROD_4.save()
 

	
 
        # Burn through some carts -- this made some past flag tests fail
 
        current_cart = TestingCartController.for_user(cls.USER_1)
 

	
 
        current_cart.next_cart()
 

	
 
        current_cart = TestingCartController.for_user(cls.USER_2)
 

	
 
        current_cart.next_cart()
 

	
 
    @classmethod
 
    def make_ceiling(cls, name, limit=None, start_time=None, end_time=None):
 
        limit_ceiling = conditions.TimeOrStockLimitFlag.objects.create(
 
            description=name,
 
            condition=conditions.FlagBase.DISABLE_IF_FALSE,
 
            limit=limit,
 
            start_time=start_time,
 
            end_time=end_time
 
        )
 
        limit_ceiling.products.add(cls.PROD_1, cls.PROD_2)
 

	
 
    @classmethod
 
    def make_category_ceiling(
 
            cls, name, limit=None, start_time=None, end_time=None):
 
        limit_ceiling = conditions.TimeOrStockLimitFlag.objects.create(
 
            description=name,
 
            condition=conditions.FlagBase.DISABLE_IF_FALSE,
 
            limit=limit,
 
            start_time=start_time,
 
            end_time=end_time
 
        )
 
        limit_ceiling.categories.add(cls.CAT_1)
 

	
 
    @classmethod
 
    def make_discount_ceiling(
 
            cls, name, limit=None, start_time=None, end_time=None,
 
            percentage=100):
 
        limit_ceiling = conditions.TimeOrStockLimitDiscount.objects.create(
 
            description=name,
 
            start_time=start_time,
 
            end_time=end_time,
 
            limit=limit,
 
        )
 
        conditions.DiscountForProduct.objects.create(
 
            discount=limit_ceiling,
 
            product=cls.PROD_1,
 
            percentage=percentage,
 
            quantity=10,
 
        )
 

	
 
    @classmethod
 
    def new_voucher(self, code="VOUCHER", limit=1):
 
        voucher = inventory.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code=code,
 
            limit=limit,
 
        )
 
        return voucher
 

	
 
    @classmethod
 
    def reget(cls, object):
 
        return type(object).objects.get(id=object.id)
 

	
 

	
 
class BasicCartTests(RegistrationCartTestCase):
 

	
 
    def test_get_cart(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        current_cart.next_cart()
 

	
 
        old_cart = current_cart
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        self.assertNotEqual(old_cart.cart, current_cart.cart)
 

	
 
        current_cart2 = TestingCartController.for_user(self.USER_1)
 
        self.assertEqual(current_cart.cart, current_cart2.cart)
 

	
 
    def test_add_to_cart_collapses_product_items(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        # Add a product twice
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # Count of products for a given user should be collapsed.
 
        items = commerce.ProductItem.objects.filter(
 
            cart=current_cart.cart,
 
            product=self.PROD_1)
 
        self.assertEqual(1, len(items))
 
        item = items[0]
 
        self.assertEquals(2, item.quantity)
 
        self.assertEqual(2, item.quantity)
 

	
 
    def test_set_quantity(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        def get_item():
 
            return commerce.ProductItem.objects.get(
 
                cart=current_cart.cart,
 
                product=self.PROD_1)
 

	
 
        current_cart.set_quantity(self.PROD_1, 1)
 
        self.assertEqual(1, get_item().quantity)
 

	
 
        # Setting the quantity to zero should remove the entry from the cart.
 
        current_cart.set_quantity(self.PROD_1, 0)
 
        with self.assertRaises(ObjectDoesNotExist):
 
            get_item()
 

	
 
        current_cart.set_quantity(self.PROD_1, 9)
 
        self.assertEqual(9, get_item().quantity)
 

	
 
        with self.assertRaises(ValidationError):
 
            current_cart.set_quantity(self.PROD_1, 11)
 

	
 
        self.assertEqual(9, get_item().quantity)
 

	
 
        with self.assertRaises(ValidationError):
 
            current_cart.set_quantity(self.PROD_1, -1)
 

	
 
        self.assertEqual(9, get_item().quantity)
 

	
 
        current_cart.set_quantity(self.PROD_1, 2)
 
        self.assertEqual(2, get_item().quantity)
 

	
 
    def test_add_to_cart_product_per_user_limit(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        # User should be able to add 1 of PROD_1 to the current cart.
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # User should be able to add 1 of PROD_1 to the current cart.
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        # User should not be able to add 10 of PROD_1 to the current cart now,
 
        # because they have a limit of 10.
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 10)
 

	
 
        current_cart.next_cart()
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        # User should not be able to add 10 of PROD_1 to the current cart now,
 
        # even though it's a new cart.
 
        with self.assertRaises(ValidationError):
 
            current_cart.add_to_cart(self.PROD_1, 10)
 

	
 
        # Second user should not be affected by first user's limits
 
        second_user_cart = TestingCartController.for_user(self.USER_2)
 
        second_user_cart.add_to_cart(self.PROD_1, 10)
 

	
 
    def set_limits(self):
 
        self.CAT_2.limit_per_user = 10
 
        self.PROD_2.limit_per_user = None
 
        self.PROD_3.limit_per_user = None
 
        self.PROD_4.limit_per_user = 6
 

	
 
        self.CAT_2.save()
 
        self.PROD_2.save()
 
        self.PROD_3.save()
 
        self.PROD_4.save()
 

	
 
    def test_per_user_product_limit_ignored_if_blank(self):
 
        self.set_limits()
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        # There is no product limit on PROD_2, and there is no cat limit
 
        current_cart.add_to_cart(self.PROD_2, 1)
 
        # There is no product limit on PROD_3, but there is a cat limit
 
        current_cart.add_to_cart(self.PROD_3, 1)
 

	
 
    def test_per_user_category_limit_ignored_if_blank(self):
 
        self.set_limits()
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        # There is no product limit on PROD_2, and there is no cat limit
 
        current_cart.add_to_cart(self.PROD_2, 1)
 
        # There is no cat limit on PROD_1, but there is a prod limit
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
    def test_per_user_category_limit_only(self):
 
        self.set_limits()
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        # Cannot add to cart if category limit is filled by one product.
 
        current_cart.set_quantity(self.PROD_3, 10)
 
        with self.assertRaises(ValidationError):
 
            current_cart.set_quantity(self.PROD_4, 1)
vendor/registrasion/registrasion/tests/test_credit_note.py
Show inline comments
...
 
@@ -21,412 +21,412 @@ class CreditNoteTestCase(TestHelperMixin, RegistrationCartTestCase):
 
    def test_overpaid_invoice_results_in_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        # Invoice is overpaid by 1 unit
 
        to_pay = invoice.invoice.value + 1
 
        invoice.pay("Reference", to_pay)
 

	
 
        # The total paid should be equal to the value of the invoice only
 
        self.assertEqual(
 
            invoice.invoice.value, invoice.invoice.total_payments()
 
        )
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        # There should be a credit note generated out of the invoice.
 
        credit_notes = commerce.CreditNote.objects.filter(
 
            invoice=invoice.invoice,
 
        )
 
        self.assertEqual(1, credit_notes.count())
 
        self.assertEqual(to_pay - invoice.invoice.value, credit_notes[0].value)
 

	
 
    def test_full_paid_invoice_does_not_generate_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        # Invoice is paid evenly
 
        invoice.pay("Reference", invoice.invoice.value)
 

	
 
        # The total paid should be equal to the value of the invoice only
 
        self.assertEqual(
 
            invoice.invoice.value, invoice.invoice.total_payments()
 
        )
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        # There should be no credit notes
 
        credit_notes = commerce.CreditNote.objects.filter(
 
            invoice=invoice.invoice,
 
        )
 
        self.assertEqual(0, credit_notes.count())
 

	
 
    def test_refund_partially_paid_invoice_generates_correct_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        # Invoice is underpaid by 1 unit
 
        to_pay = invoice.invoice.value - 1
 
        invoice.pay("Reference", to_pay)
 
        invoice.refund()
 

	
 
        # The total paid should be zero
 
        self.assertEqual(0, invoice.invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_void)
 

	
 
        # There should be a credit note generated out of the invoice.
 
        credit_notes = commerce.CreditNote.objects.filter(
 
            invoice=invoice.invoice,
 
        )
 
        self.assertEqual(1, credit_notes.count())
 
        self.assertEqual(to_pay, credit_notes[0].value)
 

	
 
    def test_refund_fully_paid_invoice_generates_correct_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        to_pay = invoice.invoice.value
 
        invoice.pay("Reference", to_pay)
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        invoice.refund()
 

	
 
        # The total paid should be zero
 
        self.assertEqual(0, invoice.invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_refunded)
 

	
 
        # There should be a credit note generated out of the invoice.
 
        credit_notes = commerce.CreditNote.objects.filter(
 
            invoice=invoice.invoice,
 
        )
 
        self.assertEqual(1, credit_notes.count())
 
        self.assertEqual(to_pay, credit_notes[0].value)
 

	
 
    def test_apply_credit_note_pays_invoice(self):
 

	
 
        # Create a manual invoice (stops credit notes from being auto-applied)
 
        self._manual_invoice(1)
 

	
 
        # Begin the test
 

	
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        to_pay = invoice.invoice.value
 
        invoice.pay("Reference", to_pay)
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        invoice.refund()
 

	
 
        # There should be one credit note generated out of the invoice.
 
        cn = self._credit_note_for_invoice(invoice.invoice)
 

	
 
        # That credit note should be in the unclaimed pile
 
        self.assertEquals(1, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(1, commerce.CreditNote.unclaimed().count())
 

	
 
        # Create a new (identical) cart with invoice
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 

	
 
        invoice2 = TestingInvoiceController.for_cart(self.reget(cart.cart))
 

	
 
        cn.apply_to_invoice(invoice2.invoice)
 
        self.assertTrue(invoice2.invoice.is_paid)
 

	
 
        # That invoice should not show up as unclaimed any more
 
        self.assertEquals(0, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(0, commerce.CreditNote.unclaimed().count())
 

	
 
    def test_apply_credit_note_generates_new_credit_note_if_overpaying(self):
 

	
 
        # Create and refund an invoice, generating a credit note.
 
        invoice = self._invoice_containing_prod_1(2)
 

	
 
        invoice.pay("Reference", invoice.invoice.value)
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        invoice.refund()
 

	
 
        # There should be one credit note generated out of the invoice.
 
        cn = self._credit_note_for_invoice(invoice.invoice)  # noqa
 

	
 
        self.assertEquals(1, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(1, commerce.CreditNote.unclaimed().count())
 

	
 
        # Create a new invoice for a cart of half value of inv 1
 
        invoice2 = self._invoice_containing_prod_1(1)
 
        # Credit note is automatically applied by generating the new invoice
 
        self.assertTrue(invoice2.invoice.is_paid)
 

	
 
        # We generated a new credit note, and spent the old one,
 
        # unclaimed should still be 1.
 
        self.assertEquals(1, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(1, commerce.CreditNote.unclaimed().count())
 

	
 
        credit_note2 = commerce.CreditNote.objects.get(
 
            invoice=invoice2.invoice,
 
        )
 

	
 
        # The new credit note should be the residual of the cost of cart 1
 
        # minus the cost of cart 2.
 
        self.assertEquals(
 
        self.assertEqual(
 
            invoice.invoice.value - invoice2.invoice.value,
 
            credit_note2.value,
 
        )
 

	
 
    def test_cannot_apply_credit_note_on_invalid_invoices(self):
 

	
 
        # Disable auto-application of invoices.
 
        self._manual_invoice(1)
 

	
 
        # And now start the actual test.
 

	
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        to_pay = invoice.invoice.value
 
        invoice.pay("Reference", to_pay)
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        invoice.refund()
 

	
 
        # There should be one credit note generated out of the invoice.
 
        cn = self._credit_note_for_invoice(invoice.invoice)
 

	
 
        # Create a new cart with invoice, pay it
 
        invoice_2 = self._invoice_containing_prod_1(1)
 
        invoice_2.pay("LOL", invoice_2.invoice.value)
 

	
 
        # Cannot pay paid invoice
 
        with self.assertRaises(ValidationError):
 
            cn.apply_to_invoice(invoice_2.invoice)
 

	
 
        invoice_2.refund()
 
        # Cannot pay refunded invoice
 
        with self.assertRaises(ValidationError):
 
            cn.apply_to_invoice(invoice_2.invoice)
 

	
 
        # Create a new cart with invoice
 
        invoice_2 = self._invoice_containing_prod_1(1)
 
        invoice_2.void()
 
        # Cannot pay void invoice
 
        with self.assertRaises(ValidationError):
 
            cn.apply_to_invoice(invoice_2.invoice)
 

	
 
    def test_cannot_apply_a_refunded_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        to_pay = invoice.invoice.value
 
        invoice.pay("Reference", to_pay)
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        invoice.refund()
 

	
 
        self.assertEquals(1, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(1, commerce.CreditNote.unclaimed().count())
 

	
 
        cn = self._credit_note_for_invoice(invoice.invoice)
 

	
 
        cn.refund()
 

	
 
        # Refunding a credit note should mark it as claimed
 
        self.assertEquals(0, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(0, commerce.CreditNote.unclaimed().count())
 

	
 
        # Create a new cart with invoice
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 

	
 
        invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart))
 

	
 
        # Cannot pay with this credit note.
 
        with self.assertRaises(ValidationError):
 
            cn.apply_to_invoice(invoice_2.invoice)
 

	
 
    def test_cannot_refund_an_applied_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        to_pay = invoice.invoice.value
 
        invoice.pay("Reference", to_pay)
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        invoice.refund()
 

	
 
        self.assertEquals(1, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(1, commerce.CreditNote.unclaimed().count())
 

	
 
        cn = self._credit_note_for_invoice(invoice.invoice)
 

	
 
        # Create a new cart with invoice
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 

	
 
        invoice_2 = TestingInvoiceController.for_cart(self.reget(cart.cart))
 
        with self.assertRaises(ValidationError):
 
            # Creating `invoice_2` will automatically apply `cn`.
 
            cn.apply_to_invoice(invoice_2.invoice)
 

	
 
        self.assertEquals(0, commerce.CreditNote.unclaimed().count())
 
        self.assertEqual(0, commerce.CreditNote.unclaimed().count())
 

	
 
        # Cannot refund this credit note as it is already applied.
 
        with self.assertRaises(ValidationError):
 
            cn.refund()
 

	
 
    def test_money_into_void_invoice_generates_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 
        invoice.void()
 

	
 
        val = invoice.invoice.value
 

	
 
        invoice.pay("Paying into the void.", val, pre_validate=False)
 
        cn = self._credit_note_for_invoice(invoice.invoice)
 
        self.assertEqual(val, cn.credit_note.value)
 

	
 
    def test_money_into_refunded_invoice_generates_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        val = invoice.invoice.value
 

	
 
        invoice.pay("Paying the first time.", val)
 
        invoice.refund()
 

	
 
        cnval = val - 1
 
        invoice.pay("Paying into the void.", cnval, pre_validate=False)
 

	
 
        notes = commerce.CreditNote.objects.filter(invoice=invoice.invoice)
 
        notes = sorted(notes, key=lambda note: note.value)
 

	
 
        self.assertEqual(cnval, notes[0].value)
 
        self.assertEqual(val, notes[1].value)
 

	
 
    def test_money_into_paid_invoice_generates_credit_note(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        val = invoice.invoice.value
 

	
 
        invoice.pay("Paying the first time.", val)
 

	
 
        invoice.pay("Paying into the void.", val, pre_validate=False)
 
        cn = self._credit_note_for_invoice(invoice.invoice)
 
        self.assertEqual(val, cn.credit_note.value)
 

	
 
    def test_invoice_with_credit_note_applied_is_refunded(self):
 
        ''' Invoices with partial payments should void when cart is updated.
 

	
 
        Test for issue #64 -- applying a credit note to an invoice
 
        means that invoice cannot be voided, and new invoices cannot be
 
        created. '''
 

	
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        # Now get a credit note
 
        invoice.pay("Lol", invoice.invoice.value)
 
        invoice.refund()
 
        cn = self._credit_note_for_invoice(invoice.invoice)
 

	
 
        # Create a cart of higher value than the credit note
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 2)
 

	
 
        # Create a current invoice
 
        # This will automatically apply `cn` to the invoice
 
        invoice = TestingInvoiceController.for_cart(cart.cart)
 

	
 
        # Adding to cart will mean that the old invoice for this cart
 
        # will be invalidated. A new invoice should be generated.
 
        cart.add_to_cart(self.PROD_1, 1)
 
        invoice = TestingInvoiceController.for_id(invoice.invoice.id)
 
        invoice2 = TestingInvoiceController.for_cart(cart.cart)  # noqa
 
        cn2 = self._credit_note_for_invoice(invoice.invoice)
 

	
 
        invoice._refresh()
 

	
 
        # The first invoice should be refunded
 
        self.assertEquals(
 
        self.assertEqual(
 
            commerce.Invoice.STATUS_VOID,
 
            invoice.invoice.status,
 
        )
 

	
 
        # Both credit notes should be for the same amount
 
        self.assertEquals(
 
        self.assertEqual(
 
            cn.credit_note.value,
 
            cn2.credit_note.value,
 
        )
 

	
 
    def test_creating_invoice_automatically_applies_credit_note(self):
 
        ''' Single credit note is automatically applied to new invoices. '''
 

	
 
        invoice = self._invoice_containing_prod_1(1)
 
        invoice.pay("boop", invoice.invoice.value)
 
        invoice.refund()
 

	
 
        # Generate a new invoice to the same value as first invoice
 
        # Should be paid, because we're applying credit notes automatically
 
        invoice2 = self._invoice_containing_prod_1(1)
 
        self.assertTrue(invoice2.invoice.is_paid)
 

	
 
    def _generate_multiple_credit_notes(self):
 
        invoice1 = self._manual_invoice(11)
 
        invoice2 = self._manual_invoice(11)
 
        invoice1.pay("Pay", invoice1.invoice.value)
 
        invoice1.refund()
 
        invoice2.pay("Pay", invoice2.invoice.value)
 
        invoice2.refund()
 
        return invoice1.invoice.value + invoice2.invoice.value
 

	
 
    def test_mutiple_credit_notes_are_applied_when_generating_invoice_1(self):
 
        ''' Tests (1) that multiple credit notes are applied to new invoice.
 

	
 
        Sum of credit note values will be *LESS* than the new invoice.
 
        '''
 

	
 
        notes_value = self._generate_multiple_credit_notes()
 
        invoice = self._manual_invoice(notes_value + 1)
 

	
 
        self.assertEqual(notes_value, invoice.invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_unpaid)
 

	
 
        user_unclaimed = commerce.CreditNote.unclaimed()
 
        user_unclaimed = user_unclaimed.filter(invoice__user=self.USER_1)
 
        self.assertEqual(0, user_unclaimed.count())
 

	
 
    def test_mutiple_credit_notes_are_applied_when_generating_invoice_2(self):
 
        ''' Tests (2) that multiple credit notes are applied to new invoice.
 

	
 
        Sum of credit note values will be *GREATER* than the new invoice.
 
        '''
 

	
 
        notes_value = self._generate_multiple_credit_notes()
 
        invoice = self._manual_invoice(notes_value - 1)
 

	
 
        self.assertEqual(notes_value - 1, invoice.invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        user_unclaimed = commerce.CreditNote.unclaimed().filter(
 
            invoice__user=self.USER_1
 
        )
 
        self.assertEqual(1, user_unclaimed.count())
 

	
 
        excess = self._credit_note_for_invoice(invoice.invoice)
 
        self.assertEqual(excess.credit_note.value, 1)
 

	
 
    def test_credit_notes_are_left_over_if_not_all_are_needed(self):
 
        ''' Tests that excess credit notes are untouched if they're not needed
 
        '''
 

	
 
        notes_value = self._generate_multiple_credit_notes()  # noqa
 
        notes_old = commerce.CreditNote.unclaimed().filter(
 
            invoice__user=self.USER_1
 
        )
 

	
 
        # Create a manual invoice whose value is smaller than any of the
 
        # credit notes we created
 
        invoice = self._manual_invoice(1)  # noqa
 
        notes_new = commerce.CreditNote.unclaimed().filter(
 
            invoice__user=self.USER_1
 
        )
 

	
 
        # Item is True if the note was't consumed when generating invoice.
 
        note_was_unused = [(i in notes_old) for i in notes_new]
 
        self.assertIn(True, note_was_unused)
 

	
 
    def test_credit_notes_are_not_applied_if_user_has_multiple_invoices(self):
 

	
 
        # Have an invoice pending with no credit notes; no payment will be made
 
        invoice1 = self._invoice_containing_prod_1(1)  # noqa
 
        # Create some credit notes.
 
        self._generate_multiple_credit_notes()
 

	
 
        invoice = self._manual_invoice(2)
 

	
 
        # Because there's already an invoice open for this user
 
        # The credit notes are not automatically applied.
 
        self.assertEqual(0, invoice.invoice.total_payments())
 
        self.assertTrue(invoice.invoice.is_unpaid)
 

	
 
    def test_credit_notes_are_applied_even_if_some_notes_are_claimed(self):
vendor/registrasion/registrasion/tests/test_invoice.py
Show inline comments
...
 
@@ -5,368 +5,369 @@ from decimal import Decimal
 
from django.core.exceptions import ValidationError
 

	
 
from registrasion.models import commerce
 
from registrasion.models import conditions
 
from registrasion.models import inventory
 
from registrasion.tests.controller_helpers import TestingCartController
 
from registrasion.tests.controller_helpers import TestingInvoiceController
 
from registrasion.tests.test_helpers import TestHelperMixin
 

	
 
from registrasion.tests.test_cart import RegistrationCartTestCase
 

	
 
UTC = pytz.timezone('UTC')
 

	
 

	
 
class InvoiceTestCase(TestHelperMixin, RegistrationCartTestCase):
 

	
 
    def test_create_invoice(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        # Should be able to create an invoice after the product is added
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
 
        # That invoice should have a single line item
 
        line_items = commerce.LineItem.objects.filter(
 
            invoice=invoice_1.invoice,
 
        )
 
        self.assertEqual(1, len(line_items))
 
        # That invoice should have a value equal to cost of PROD_1
 
        self.assertEqual(self.PROD_1.price, invoice_1.invoice.value)
 

	
 
        # Adding item to cart should produce a new invoice
 
        current_cart.add_to_cart(self.PROD_2, 1)
 
        invoice_2 = TestingInvoiceController.for_cart(current_cart.cart)
 
        self.assertNotEqual(invoice_1.invoice, invoice_2.invoice)
 

	
 
        # The old invoice should automatically be voided
 
        invoice_1_new = commerce.Invoice.objects.get(pk=invoice_1.invoice.id)
 
        invoice_2_new = commerce.Invoice.objects.get(pk=invoice_2.invoice.id)
 
        self.assertTrue(invoice_1_new.is_void)
 
        self.assertFalse(invoice_2_new.is_void)
 

	
 
        # Invoice should have two line items
 
        line_items = commerce.LineItem.objects.filter(
 
            invoice=invoice_2.invoice,
 
        )
 
        self.assertEqual(2, len(line_items))
 
        # Invoice should have a value equal to cost of PROD_1 and PROD_2
 
        self.assertEqual(
 
            self.PROD_1.price + self.PROD_2.price,
 
            invoice_2.invoice.value)
 

	
 
    def test_invoice_controller_for_id_works(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        id_ = invoice.invoice.id
 

	
 
        invoice1 = TestingInvoiceController.for_id(id_)
 
        invoice2 = TestingInvoiceController.for_id(str(id_))
 

	
 
        self.assertEqual(invoice.invoice, invoice1.invoice)
 
        self.assertEqual(invoice.invoice, invoice2.invoice)
 

	
 
    def test_create_invoice_fails_if_cart_invalid(self):
 
        self.make_ceiling("Limit ceiling", limit=1)
 
        self.set_time(datetime.datetime(2015, 1, 1, tzinfo=UTC))
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.add_to_cart(self.PROD_1, 1)
 

	
 
        self.add_timedelta(self.RESERVATION * 2)
 
        cart_2 = TestingCartController.for_user(self.USER_2)
 
        cart_2.add_to_cart(self.PROD_1, 1)
 

	
 
        # Now try to invoice the first user
 
        with self.assertRaises(ValidationError):
 
            TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
    def test_paying_invoice_makes_new_cart(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        invoice.pay("A payment!", invoice.invoice.value)
 

	
 
        # This payment is for the correct amount invoice should be paid.
 
        self.assertTrue(invoice.invoice.is_paid)
 

	
 
        # Cart should not be active
 
        self.assertNotEqual(
 
            commerce.Cart.STATUS_ACTIVE,
 
            invoice.invoice.cart.status,
 
        )
 

	
 
        # Asking for a cart should generate a new one
 
        new_cart = TestingCartController.for_user(self.USER_1)
 
        self.assertNotEqual(invoice.invoice.cart, new_cart.cart)
 

	
 
    def test_total_payments_balance_due(self):
 
        invoice = self._invoice_containing_prod_1(2)
 
        for i in xrange(0, invoice.invoice.value):
 
        invoice_value = int(invoice.invoice.value)
 
        for i in range(0, invoice_value):
 
            self.assertTrue(
 
                i + 1, invoice.invoice.total_payments()
 
            )
 
            self.assertTrue(
 
                invoice.invoice.value - i, invoice.invoice.balance_due()
 
            )
 
            invoice.pay("Pay 1", 1)
 

	
 
    def test_invoice_includes_discounts(self):
 
        voucher = inventory.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code="VOUCHER",
 
            limit=1
 
        )
 
        discount = conditions.VoucherDiscount.objects.create(
 
            description="VOUCHER RECIPIENT",
 
            voucher=voucher,
 
        )
 
        conditions.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=self.PROD_1,
 
            percentage=Decimal(50),
 
            quantity=1
 
        )
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.apply_voucher(voucher.code)
 

	
 
        # Should be able to create an invoice after the product is added
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        # That invoice should have two line items
 
        line_items = commerce.LineItem.objects.filter(
 
            invoice=invoice_1.invoice,
 
        )
 
        self.assertEqual(2, len(line_items))
 
        # That invoice should have a value equal to 50% of the cost of PROD_1
 
        self.assertEqual(
 
            self.PROD_1.price * Decimal("0.5"),
 
            invoice_1.invoice.value)
 

	
 
    def _make_zero_value_invoice(self):
 
        voucher = inventory.Voucher.objects.create(
 
            recipient="Voucher recipient",
 
            code="VOUCHER",
 
            limit=1
 
        )
 
        discount = conditions.VoucherDiscount.objects.create(
 
            description="VOUCHER RECIPIENT",
 
            voucher=voucher,
 
        )
 
        conditions.DiscountForProduct.objects.create(
 
            discount=discount,
 
            product=self.PROD_1,
 
            percentage=Decimal(100),
 
            quantity=1
 
        )
 

	
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        current_cart.apply_voucher(voucher.code)
 

	
 
        # Should be able to create an invoice after the product is added
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        return TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
    def test_zero_value_invoice_is_automatically_paid(self):
 
        invoice_1 = self._make_zero_value_invoice()
 
        self.assertTrue(invoice_1.invoice.is_paid)
 

	
 
    def test_refunding_zero_value_invoice_releases_cart(self):
 
        invoice_1 = self._make_zero_value_invoice()
 
        cart = invoice_1.invoice.cart
 
        invoice_1.refund()
 

	
 
        cart.refresh_from_db()
 
        self.assertEquals(commerce.Cart.STATUS_RELEASED, cart.status)
 
        self.assertEqual(commerce.Cart.STATUS_RELEASED, cart.status)
 

	
 
    def test_invoice_voids_self_if_cart_changes(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 

	
 
        # Should be able to create an invoice after the product is added
 
        current_cart.add_to_cart(self.PROD_1, 1)
 
        invoice_1 = TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
        self.assertFalse(invoice_1.invoice.is_void)
 

	
 
        # Adding item to cart should produce a new invoice
 
        current_cart.add_to_cart(self.PROD_2, 1)
 
        invoice_2 = TestingInvoiceController.for_cart(current_cart.cart)
 
        self.assertNotEqual(invoice_1.invoice, invoice_2.invoice)
 

	
 
        # Viewing invoice_1's invoice should show it as void
 
        invoice_1_new = TestingInvoiceController(invoice_1.invoice)
 
        self.assertTrue(invoice_1_new.invoice.is_void)
 

	
 
        # Viewing invoice_2's invoice should *not* show it as void
 
        invoice_2_new = TestingInvoiceController(invoice_2.invoice)
 
        self.assertFalse(invoice_2_new.invoice.is_void)
 

	
 
    def test_invoice_voids_self_if_cart_becomes_invalid(self):
 
        ''' Invoices should be void if cart becomes invalid over time '''
 

	
 
        self.make_ceiling("Limit ceiling", limit=1)
 
        self.set_time(datetime.datetime(
 
            year=2015, month=1, day=1, hour=0, minute=0, tzinfo=UTC,
 
        ))
 

	
 
        cart1 = TestingCartController.for_user(self.USER_1)
 
        cart2 = TestingCartController.for_user(self.USER_2)
 

	
 
        # Create a valid invoice for USER_1
 
        cart1.add_to_cart(self.PROD_1, 1)
 
        inv1 = TestingInvoiceController.for_cart(cart1.cart)
 

	
 
        # Expire the reservations, and have USER_2 take up PROD_1's ceiling
 
        # generate an invoice
 
        self.add_timedelta(self.RESERVATION * 2)
 
        cart2.add_to_cart(self.PROD_2, 1)
 
        TestingInvoiceController.for_cart(cart2.cart)
 

	
 
        # Re-get inv1's invoice; it should void itself on loading.
 
        inv1 = TestingInvoiceController(inv1.invoice)
 
        self.assertTrue(inv1.invoice.is_void)
 

	
 
    def test_voiding_invoice_creates_new_invoice(self):
 
        invoice_1 = self._invoice_containing_prod_1(1)
 

	
 
        self.assertFalse(invoice_1.invoice.is_void)
 
        invoice_1.void()
 

	
 
        invoice_2 = TestingInvoiceController.for_cart(invoice_1.invoice.cart)
 
        self.assertNotEqual(invoice_1.invoice, invoice_2.invoice)
 

	
 
    def test_cannot_pay_void_invoice(self):
 
        invoice_1 = self._invoice_containing_prod_1(1)
 

	
 
        invoice_1.void()
 

	
 
        with self.assertRaises(ValidationError):
 
            invoice_1.validate_allowed_to_pay()
 

	
 
    def test_cannot_void_paid_invoice(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        invoice.pay("Reference", invoice.invoice.value)
 

	
 
        with self.assertRaises(ValidationError):
 
            invoice.void()
 

	
 
    def test_cannot_void_partially_paid_invoice(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        invoice.pay("Reference", invoice.invoice.value - 1)
 
        self.assertTrue(invoice.invoice.is_unpaid)
 

	
 
        with self.assertRaises(ValidationError):
 
            invoice.void()
 

	
 
    def test_cannot_generate_blank_invoice(self):
 
        current_cart = TestingCartController.for_user(self.USER_1)
 
        with self.assertRaises(ValidationError):
 
            TestingInvoiceController.for_cart(current_cart.cart)
 

	
 
    def test_cannot_pay_implicitly_void_invoice(self):
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_1, 1)
 
        invoice = TestingInvoiceController.for_cart(self.reget(cart.cart))
 

	
 
        # Implicitly void the invoice
 
        cart.add_to_cart(self.PROD_1, 1)
 

	
 
        with self.assertRaises(ValidationError):
 
            invoice.validate_allowed_to_pay()
 

	
 
    def test_required_category_constraints_prevent_invoicing(self):
 
        self.CAT_1.required = True
 
        self.CAT_1.save()
 

	
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_3, 1)
 

	
 
        # CAT_1 is required, we don't have CAT_1 yet
 
        with self.assertRaises(ValidationError):
 
            invoice = TestingInvoiceController.for_cart(cart.cart)
 

	
 
        # Now that we have CAT_1, we can check out the cart
 
        cart.add_to_cart(self.PROD_1, 1)
 
        invoice = TestingInvoiceController.for_cart(cart.cart)
 

	
 
        # Paying for the invoice should work fine
 
        invoice.pay("Boop", invoice.invoice.value)
 

	
 
        # We have an item in the first cart, so should be able to invoice
 
        # for the second cart, even without CAT_1 in it.
 
        cart = TestingCartController.for_user(self.USER_1)
 
        cart.add_to_cart(self.PROD_3, 1)
 

	
 
        invoice2 = TestingInvoiceController.for_cart(cart.cart)
 

	
 
        # Void invoice2, and release the first cart
 
        # now we don't have any CAT_1
 
        invoice2.void()
 
        invoice.refund()
 

	
 
        # Now that we don't have CAT_1, we can't checkout this cart
 
        with self.assertRaises(ValidationError):
 
            invoice = TestingInvoiceController.for_cart(cart.cart)
 

	
 
    def test_can_generate_manual_invoice(self):
 

	
 
        description_price_pairs = [
 
            ("Item 1", 15),
 
            ("Item 2", 30),
 
        ]
 

	
 
        due_delta = datetime.timedelta(hours=24)
 

	
 
        _invoice = TestingInvoiceController.manual_invoice(
 
            self.USER_1, due_delta, description_price_pairs
 
        )
 
        inv = TestingInvoiceController(_invoice)
 

	
 
        self.assertEquals(
 
        self.assertEqual(
 
            inv.invoice.value,
 
            sum(i[1] for i in description_price_pairs)
 
        )
 

	
 
        self.assertEquals(
 
        self.assertEqual(
 
            len(inv.invoice.lineitem_set.all()),
 
            len(description_price_pairs)
 
        )
 

	
 
        inv.pay("Demo payment", inv.invoice.value)
 

	
 
    def test_sends_email_on_invoice_creation(self):
 
        invoice = self._invoice_containing_prod_1(1)
 
        self.assertEquals(1, len(self.emails))
 
        self.assertEqual(1, len(self.emails))
 
        email = self.emails[0]
 
        self.assertEquals([self.USER_1.email], email["to"])
 
        self.assertEquals("invoice_created", email["kind"])
 
        self.assertEquals(invoice.invoice, email["context"]["invoice"])
 
        self.assertEqual([self.USER_1.email], email["to"])
 
        self.assertEqual("invoice_created", email["kind"])
 
        self.assertEqual(invoice.invoice, email["context"]["invoice"])
 

	
 
    def test_sends_first_change_email_on_invoice_fully_paid(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        self.assertEquals(1, len(self.emails))
 
        self.assertEqual(1, len(self.emails))
 
        invoice.pay("Partial", invoice.invoice.value - 1)
 
        # Should have an "invoice_created" email and nothing else.
 
        self.assertEquals(1, len(self.emails))
 
        self.assertEqual(1, len(self.emails))
 
        invoice.pay("Remainder", 1)
 
        self.assertEquals(2, len(self.emails))
 
        self.assertEqual(2, len(self.emails))
 

	
 
        email = self.emails[1]
 
        self.assertEquals([self.USER_1.email], email["to"])
 
        self.assertEquals("invoice_updated", email["kind"])
 
        self.assertEquals(invoice.invoice, email["context"]["invoice"])
 
        self.assertEqual([self.USER_1.email], email["to"])
 
        self.assertEqual("invoice_updated", email["kind"])
 
        self.assertEqual(invoice.invoice, email["context"]["invoice"])
 

	
 
    def test_sends_email_when_invoice_refunded(self):
 
        invoice = self._invoice_containing_prod_1(1)
 

	
 
        self.assertEquals(1, len(self.emails))
 
        self.assertEqual(1, len(self.emails))
 
        invoice.pay("Payment", invoice.invoice.value)
 
        self.assertEquals(2, len(self.emails))
 
        self.assertEqual(2, len(self.emails))
 
        invoice.refund()
 
        self.assertEquals(3, len(self.emails))
 
        self.assertEqual(3, len(self.emails))
 

	
 
        email = self.emails[2]
 
        self.assertEquals([self.USER_1.email], email["to"])
 
        self.assertEquals("invoice_updated", email["kind"])
 
        self.assertEquals(invoice.invoice, email["context"]["invoice"])
 
        self.assertEqual([self.USER_1.email], email["to"])
 
        self.assertEqual("invoice_updated", email["kind"])
 
        self.assertEqual(invoice.invoice, email["context"]["invoice"])
vendor/registrasion/registrasion/tests/test_speaker.py
Show inline comments
 
import pytz
 

	
 
from registrasion.models import conditions
 
from registrasion.controllers.product import ProductController
 

	
 
from symposion.conference import models as conference_models
 
from symposion.proposals import models as proposal_models
 
from symposion.reviews.models import promote_proposal
 
from symposion.schedule import models as schedule_models
 
from symposion.speakers import models as speaker_models
 

	
 

	
 
from registrasion.tests.test_cart import RegistrationCartTestCase
 

	
 
UTC = pytz.timezone('UTC')
 

	
 

	
 
class SpeakerTestCase(RegistrationCartTestCase):
 

	
 
    @classmethod
 
    def _create_proposals(cls):
 
        ''' Creates two proposals:
 

	
 
        - User 1 will be presenter
 
        - User 2 will be an additional presenter
 

	
 
        Each proposal is of a different ProposalKind.
 
        '''
 

	
 
        conference = conference_models.Conference.objects.create(
 
            title="TEST CONFERENCE.",
 
        )
 
        section = conference_models.Section.objects.create(
 
            conference=conference,
 
            name="TEST_SECTION",
 
            slug="testsection",
 
        )
 
        proposal_section = proposal_models.ProposalSection.objects.create(  # noqa
 
            section=section,
 
            closed=False,
 
            published=False,
 
        )
 

	
 
        kind_1 = proposal_models.ProposalKind.objects.create(
 
            section=section,
 
            name="Kind 1",
 
            slug="kind1",
 
        )
 
        kind_2 = proposal_models.ProposalKind.objects.create(
 
            section=section,
 
            name="Kind 2",
 
            slug="kind2",
 
        )
 

	
 
        speaker_1 = speaker_models.Speaker.objects.create(
 
            user=cls.USER_1,
 
            name="Speaker 1",
 
            annotation="",
 
        )
 
        speaker_2 = speaker_models.Speaker.objects.create(
 
            user=cls.USER_2,
 
            name="Speaker 2",
 
            annotation="",
 
        )
 

	
 
        proposal_1 = proposal_models.ProposalBase.objects.create(
 
            kind=kind_1,
 
            title="Proposal 1",
 
            abstract="Abstract",
 
            description="Description",
 
            #description="Description",
 
            speaker=speaker_1,
 
        )
 
        proposal_models.AdditionalSpeaker.objects.create(
 
            speaker=speaker_2,
 
            proposalbase=proposal_1,
 
            status=proposal_models.AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED,
 
        )
 

	
 
        proposal_2 = proposal_models.ProposalBase.objects.create(
 
            kind=kind_2,
 
            title="Proposal 2",
 
            abstract="Abstract",
 
            description="Description",
 
            #description="Description",
 
            speaker=speaker_1,
 
        )
 
        proposal_models.AdditionalSpeaker.objects.create(
 
            speaker=speaker_2,
 
            proposalbase=proposal_2,
 
            status=proposal_models.AdditionalSpeaker.SPEAKING_STATUS_ACCEPTED,
 
        )
 

	
 
        cls.KIND_1 = kind_1
 
        cls.KIND_2 = kind_2
 
        cls.PROPOSAL_1 = proposal_1
 
        cls.PROPOSAL_2 = proposal_2
 

	
 
    @classmethod
 
    def _create_flag_for_primary_speaker(cls):
 
        ''' Adds flag -- PROD_1 is not available unless user is a primary
 
        presenter of a KIND_1 '''
 
        flag = conditions.SpeakerFlag.objects.create(
 
            description="User must be presenter",
 
            condition=conditions.FlagBase.ENABLE_IF_TRUE,
 
            is_presenter=True,
 
            is_copresenter=False,
 
        )
 
        flag.proposal_kind.add(cls.KIND_1)
 
        flag.products.add(cls.PROD_1)
 

	
 
    @classmethod
 
    def _create_flag_for_additional_speaker(cls):
 
        ''' Adds flag -- PROD_1 is not available unless user is a primary
 
        presenter of a KIND_2 '''
 
        flag = conditions.SpeakerFlag.objects.create(
 
            description="User must be copresenter",
 
            condition=conditions.FlagBase.ENABLE_IF_TRUE,
 
            is_presenter=False,
 
            is_copresenter=True,
 
        )
 
        flag.proposal_kind.add(cls.KIND_1)
 
        flag.products.add(cls.PROD_1)
 

	
 
    def test_create_proposals(self):
 
        self._create_proposals()
 

	
 
        self.assertIsNotNone(self.KIND_1)
 
        self.assertIsNotNone(self.KIND_2)
 
        self.assertIsNotNone(self.PROPOSAL_1)
 
        self.assertIsNotNone(self.PROPOSAL_2)
 

	
 
    def test_primary_speaker_enables_item(self):
 
        self._create_proposals()
 
        self._create_flag_for_primary_speaker()
 

	
 
        # USER_1 cannot see PROD_1 until proposal is promoted.
 
        available = ProductController.available_products(
 
            self.USER_1,
 
            products=[self.PROD_1],
 
        )
 
        self.assertNotIn(self.PROD_1, available)
 

	
 
        # promote proposal_1 so that USER_1 becomes a speaker
 
        promote_proposal(self.PROPOSAL_1)
 

	
 
        # USER_1 can see PROD_1
 
        available_1 = ProductController.available_products(
 
            self.USER_1,
 
            products=[self.PROD_1],
 
        )
 
        self.assertIn(self.PROD_1, available_1)
 
        # USER_2 can *NOT* see PROD_1 because they're a copresenter
 
        available_2 = ProductController.available_products(
 
            self.USER_2,
 
            products=[self.PROD_1],
 
        )
 
        self.assertNotIn(self.PROD_1, available_2)
 

	
 
    def test_additional_speaker_enables_item(self):
 
        self._create_proposals()
 
        self._create_flag_for_additional_speaker()
 

	
 
        # USER_2 cannot see PROD_1 until proposal is promoted.
 
        available = ProductController.available_products(
 
            self.USER_2,
 
            products=[self.PROD_1],
 
        )
 
        self.assertNotIn(self.PROD_1, available)
 

	
 
        # promote proposal_1 so that USER_2 becomes an additional speaker
 
        promote_proposal(self.PROPOSAL_1)
 

	
 
        # USER_2 can see PROD_1
 
        available_2 = ProductController.available_products(
 
            self.USER_2,
 
            products=[self.PROD_1],
 
        )
 
        self.assertIn(self.PROD_1, available_2)
 
        # USER_1 can *NOT* see PROD_1 because they're a presenter
 
        available_1 = ProductController.available_products(
vendor/registrasion/registrasion/views.py
Show inline comments
 
from collections import defaultdict
 
import datetime
 
import zipfile
 
import os
 
import logging
 
import subprocess
 

	
 
from django.contrib import messages
 

	
 
from . import forms
 
from . import util
 
from .models import commerce
 
from .models import inventory
 
from .models import people
 
from .controllers.batch import BatchController
 
from .controllers.cart import CartController
 
from .controllers.category import CategoryController
 
from .controllers.credit_note import CreditNoteController
 
from .controllers.discount import DiscountController
 
from .controllers.invoice import InvoiceController
 
from .controllers.item import ItemController
 
from .controllers.product import ProductController
 
from .exceptions import CartValidationError
 

	
 
from collections import namedtuple
 

	
 
from django import forms as django_forms
 
from django.conf import settings
 
from django.contrib.auth.decorators import login_required
 
from django.contrib.auth.decorators import user_passes_test
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from django.contrib import messages
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.core.exceptions import ValidationError
 
from django.core.mail import send_mass_mail
 
from django.http import Http404, HttpResponse
 
from django.shortcuts import redirect
 
from django.shortcuts import render
 
from django.template import Context, Template, loader
 
from django.urls import reverse
 
import waffle
 

	
 
from lxml import etree
 
from copy import deepcopy
 

	
 
from registrasion.forms import BadgeForm, ticket_selection
 
from registrasion.contrib.badger import (
 
                                         collate,
 
                                         svg_badge,
 
                                         InvalidTicketChoiceError
 
                                         )
 

	
 
User = get_user_model()
 

	
 
_GuidedRegistrationSection = namedtuple(
 
    "GuidedRegistrationSection",
 
    (
 
        "title",
 
        "discounts",
 
        "description",
 
        "form",
 
    )
 
)
 

	
 
@util.all_arguments_optional
 
class GuidedRegistrationSection(_GuidedRegistrationSection):
 
    ''' Represents a section of a guided registration page.
 

	
 
    Attributes:
 
       title (str): The title of the section.
 

	
 
       discounts ([registrasion.contollers.discount.DiscountAndQuantity, ...]):
 
            A list of discount objects that are available in the section. You
 
            can display ``.clause`` to show what the discount applies to, and
 
            ``.quantity`` to display the number of times that discount can be
 
            applied.
 

	
 
       description (str): A description of the section.
 

	
 
       form (forms.Form): A form to display.
 
    '''
 
    pass
 

	
 

	
 
@login_required
 
def guided_registration(request, page_number=None):
 
    ''' Goes through the registration process in order, making sure user sees
 
    all valid categories.
 

	
 
    The user must be logged in to see this view.
 

	
 
    Parameter:
 
        page_number:
 
            1) Profile form (and e-mail address?)
 
            2) Ticket type
 
            3) T&C Consent
 
            4) Remaining products
 
            5) Mark registration as complete
 

	
 
    Returns:
 
        render: Renders ``registrasion/guided_registration.html``,
 
            with the following data::
 

	
 
                {
 
                    "previous_step": int(),  # Previous step
 
                    "current_step": int(),  # The current step in the
 
                                            # registration
 
                    "sections": sections,   # A list of
 
                                            # GuidedRegistrationSections
 
                    "title": str(),         # The title of the page
 
                    "total_steps": int(),   # The total number of steps
 
                }
 

	
 
    '''
 

	
 
    PAGE_PROFILE = 1
 
    PAGE_TICKET = 2
 
    PAGE_TERMS = 3
 
    PAGE_PRODUCTS = 4
 
    PAGE_PRODUCTS_MAX = 5
 
    TOTAL_PAGES = 5
 

	
 
    ticket_category = inventory.Category.objects.get(
 
        id=settings.TICKET_PRODUCT_CATEGORY
 
    )
 
    cart = CartController.for_user(request.user)
 

	
 
    attendee = people.Attendee.get_instance(request.user)
 

	
 
    # This guided registration process is only for people who have
 
    # not completed registration (and has confusing behaviour if you go
 
    # back to it.)
 
    if attendee.completed_registration:
 
        return redirect(review)
 

	
 
    # Calculate the current maximum page number for this user.
 
    has_profile = hasattr(attendee, "attendeeprofilebase")
 
    if not has_profile:
 
        # If there's no profile, they have to go to the profile page.
 
        max_page = PAGE_PROFILE
 
        redirect_page = PAGE_PROFILE
 
    else:
 
        # We have a profile.
 
        # Do they have a ticket?
 
        products = inventory.Product.objects.filter(
 
            productitem__cart=cart.cart
 
        )
 
        tickets = products.filter(category=ticket_category)
 

	
 
        if tickets.count() == 0:
vendor/registrasion/requirements/base.txt
Show inline comments
 
django-nested-admin==2.2.6
 
django-nested-admin==3.3.2
 
#symposion==1.0b2.dev3
 
lxml==4.0.0
 
lxml==4.6.1
vendor/registripe/registripe/forms.py
Show inline comments
 
import copy
 
from registripe import models
 

	
 
from django import forms
 
from django.core.urlresolvers import reverse
 
import functools
 

	
 
from django import forms
 
from django.core.urlresolvers import reverse
 
from django.core.exceptions import ValidationError
 
from django.db.models import F, Q
 
from django.forms import widgets
 
from django.urls import reverse
 
from django.utils import timezone
 

	
 
from django_countries import countries
 
from django_countries.fields import LazyTypedChoiceField
 
from django_countries.widgets import CountrySelectWidget
 

	
 
from pinax.stripe import models as pinax_stripe_models
 

	
 

	
 
class StripeCardElement(forms.widgets.TextInput):
 

	
 
    def render(self, name, value, attrs=None):
 
    def render(self, name, value, attrs=None, renderer=None):
 
        element = '''
 
            <div class="registrasion-stripe-element" id='%s' style='"-moz-appearance: textfield; -webkit-appearance: textfield;     appearance: field;"'>Please wait.</div>''' % (name, )
 

	
 
        script = '''
 
            <script type='text/javascript'>
 
                window.addEventListener('load', function(event){
 
                    stripeify('%s');
 
                });
 
            </script>''' % (name)
 
        return element + script
 

	
 

	
 
class StripeTokenWidget(forms.widgets.HiddenInput):
 

	
 
    def render(self, name, value, attrs=None):
 
    def render(self, name, value, attrs=None, renderer=None):
 

	
 
        return '''
 
            <div class='registrasion-stripe-token' style='display:none;'
 
            data-input-id='%s'
 
            ></div>
 
        ''' % (name, )
 

	
 

	
 
class CreditCardForm(forms.Form):
 

	
 
    required_css_class = 'label-required'
 

	
 
    def _media(self):
 
        js = (
 
            'https://js.stripe.com/v3/',
 
            reverse("registripe_form_handler"),
 
        )
 

	
 
        return forms.Media(js=js)
 

	
 
    media = property(_media)
 

	
 
    card = forms.CharField(
 
        required=False,
 
        label="Credit card",
 
        max_length=255,
 
        widget=StripeCardElement()
 
    )
 

	
 
    stripe_token = forms.CharField(
 
        max_length=255,
 
        #required=True,
 
        widget=StripeTokenWidget(),
 
    )
 

	
 

	
 
class StripeRefundForm(forms.Form):
 

	
 
    def __init__(self, *args, **kwargs):
 
        '''
 

	
 
        Arguments:
 
            user (User): The user whose charges we should filter to.
 
            min_value (Decimal): The minimum value of the charges we should
 
                show (currently, credit notes can only be cashed out in full.)
 

	
 
        '''
 
        user = kwargs.pop('user', None)
 
        min_value = kwargs.pop('min_value', None)
 
        super(StripeRefundForm, self).__init__(*args, **kwargs)
 

	
 
        payment_field = self.fields['payment']
 
        qs = payment_field.queryset
 

	
 
        if user:
 
            qs = qs.filter(
 
                charge__customer__user=user,
 
            )
 

	
 
        if min_value is not None:
 
            # amount >= amount_to_refund + amount_refunded
 
            # No refunds yet
 
            q1 = (
 
                Q(charge__amount_refunded__isnull=True) &
 
                Q(charge__amount__gte=min_value)
 
            )
 
            # There are some refunds
 
            q2 = (
 
                Q(charge__amount_refunded__isnull=False) &
 
                Q(charge__amount__gte=(
 
                    F("charge__amount_refunded") + min_value)
 
                )
 
            )
 
            qs = qs.filter(q1 | q2)
 

	
 
        payment_field.queryset = qs
 

	
 
    payment = forms.ModelChoiceField(
 
        required=True,
 
        queryset=models.StripePayment.objects.all(),
 
    )
vendor/registripe/registripe/models.py
Show inline comments
 
from __future__ import unicode_literals
 

	
 
from django.db import models
 
from registrasion.models import commerce
 
from pinax.stripe.models import Charge
 

	
 

	
 
class StripePayment(commerce.PaymentBase):
 

	
 
    charge = models.ForeignKey(Charge)
 
    charge = models.ForeignKey(Charge, on_delete=models.CASCADE)
 

	
 
class StripeCreditNoteRefund(commerce.CreditNoteRefund):
 

	
 
    charge = models.ForeignKey(Charge)
 
    charge = models.ForeignKey(Charge, on_delete=models.CASCADE)
vendor/registripe/registripe/templates/registrasion/stripe/link_to_refunds_.html
Show inline comments
 
{% comment %}
 
  This is used in the default invoice.html file to display Stripe funcationality if the app is loaded.
 
  This is used in the default invoice.html file to display Stripe functionality if the app is loaded.
 
{% endcomment %}
 

	
 
{% block content %}
 
  <h3>Stripe Refund</h3>
 

	
 
  <p><a href="{% url 'registripe_refund' credit_note_id %}">View Stripe refund options</a></p>
 
{% endblock %}
vendor/registripe/requirements.txt
Show inline comments
 
django-countries>=4.0
 
pinax-stripe==3.2.1
 
requests>=2.11.1
 
stripe==1.38.0
 
django-countries>=6.1.3
 
pinax-stripe==4.4.0
 
requests>=2.24.0
 
stripe==2.55.0
vendor/symposion/conference/models.py
Show inline comments
 
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"))
 
    conference = models.ForeignKey(
 
        Conference,
 
        verbose_name=_("Conference"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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
vendor/symposion/conference/views.py
Show inline comments
 
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
 
from django.contrib.auth import get_user_model
 

	
 
User = get_user_model()
 

	
 

	
 
@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(),
 
    })
vendor/symposion/proposals/models.py
Show inline comments
 
import os
 
import uuid
 

	
 
from django.core.urlresolvers import reverse
 
from django.db import models
 
from django.db.models import Q
 
from django.urls import reverse
 
from django.utils.translation import ugettext_lazy as _
 
from django.utils.timezone import now
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
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
 

	
 
User = get_user_model()
 

	
 

	
 
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"))
 
    section = models.OneToOneField(
 
        Section,
 
        verbose_name=_("Section"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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"))
 
    section = models.ForeignKey(
 
        Section,
 
        related_name="proposal_kinds",
 
        verbose_name=_("Section"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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"))
 
    kind = models.ForeignKey(
 
        ProposalKind,
 
        verbose_name=_("Kind"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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"))
 
    speaker = models.ForeignKey(
 
        Speaker,
 
        related_name="proposals",
 
        verbose_name=_("Speaker"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    # @@@ 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"))
 
    speaker = models.ForeignKey(
 
        Speaker,
 
        verbose_name=_("Speaker"),
 
        on_delete=models.CASCADE,
 
    )
 
    proposalbase = models.ForeignKey(
 
        ProposalBase,
 
        verbose_name=_("Proposalbase"),
 
        on_delete=models.CASCADE,
 
    )
 
    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"))
 
    proposal = models.ForeignKey(
 
        ProposalBase,
 
        related_name="supporting_documents",
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    uploaded_by = models.ForeignKey(User, verbose_name=_("Uploaded by"))
 
    uploaded_by = models.ForeignKey(
 
        User,
 
        verbose_name=_("Uploaded by"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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
vendor/symposion/proposals/views.py
Show inline comments
 
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.urls import reverse
 
from django.views import static
 

	
 
from django.contrib import messages
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 

	
 
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
 
)
 

	
 
User = get_user_model()
 

	
 

	
 
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():
 
    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("dashboard")  # @@@ 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,
 
    })
 

	
 
@login_required
 
def proposal_submit_kind(request, kind_slug):
 

	
 
    kind = get_object_or_404(ProposalKind, slug=kind_slug)
 

	
 
    try:
 
        speaker_profile = request.user.speaker_profile
 
    except ObjectDoesNotExist:
 
        return redirect("speaker_create")
 

	
 
    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:
vendor/symposion/reviews/models.py
Show inline comments
 
# -*- 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.contrib.auth import get_user_model
 
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
 

	
 
User = get_user_model()
 

	
 

	
 
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"))
 
    proposal = models.ForeignKey(
 
        ProposalBase,
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 
    user = models.ForeignKey(
 
        User,
 
        verbose_name=_("User"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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"))
 
    proposal = models.ForeignKey(
 
        ProposalBase,
 
        related_name="messages",
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 
    user = models.ForeignKey(
 
        User,
 
        verbose_name=_("User"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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"))
 
    proposal = models.ForeignKey(
 
        ProposalBase,
 
        related_name="reviews",
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 
    user = models.ForeignKey(
 
        User,
 
        verbose_name=_("User"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    # 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 = 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"))
 
    proposal = models.ForeignKey(
 
        ProposalBase,
 
        related_name="votes",
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 
    user = models.ForeignKey(
 
        User,
 
        verbose_name=_("User"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    # 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"))
 
    proposal = models.OneToOneField(
 
        ProposalBase,
 
        related_name="result",
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 
    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 calculate_score(self):
 
        if self.vote_count == 0:
 
            return 0
 
        else:
 
            return ((2 * self.plus_two + self.plus_one) - (2 * self.minus_two + self.minus_one)) / self.vote_count
 

	
 
    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.score = self.calculate_score()
 
        self.save()
 

	
 
    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"))
 
    proposal = models.ForeignKey(
 
        ProposalBase,
 
        related_name="comments",
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 
    commenter = models.ForeignKey(
 
        User,
 
        verbose_name=_("Commenter"),
 
        on_delete=models.CASCADE,
 
    )
 
    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"))
 
    proposal = models.ForeignKey(
 
        ProposalBase,
 
        related_name="notifications",
 
        verbose_name=_("Proposal"),
 
        on_delete=models.CASCADE,
 
    )
 
    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)
vendor/symposion/reviews/templatetags/review_tags.py
Show inline comments
 
from django import template
 

	
 
from symposion.reviews.models import ReviewAssignment
 

	
 

	
 
register = template.Library()
 

	
 

	
 
@register.assignment_tag(takes_context=True)
 
@register.simple_tag(takes_context=True)
 
def review_assignments(context):
 
    request = context["request"]
 
    assignments = ReviewAssignment.objects.filter(user=request.user)
 
    return assignments
vendor/symposion/reviews/views.py
Show inline comments
...
 
@@ -175,204 +175,207 @@ def review_proposals_csv(request, section_slug=None):
 
    for proposal in proposals:
 

	
 
        proposal.speaker_name = proposal.speaker.name
 
        section_slug = proposal.kind.section.slug
 
        kind_slug = proposal.kind.slug
 
        proposal.proposal_type = kind_slug
 

	
 
        if hasattr(proposal, "target_audience"):
 
            proposal.audience = proposal.get_target_audience_display()
 
        else:
 
            proposal.audience = "Unknown"
 

	
 
        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()
 
        )
 

	
 
        suggested_status = proposal.status
 
        if suggested_status == "undecided":
 
            if proposal.score >= 1.5:
 
                suggested_status = "auto-accept"
 
            elif proposal.score <= -1.5:
 
                suggested_status = "auto-reject"
 
        proposal.suggested_status = suggested_status
 

	
 
        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 withdrawn talks
 
    queryset = queryset.exclude(cancelled=True)
 
    # 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.
 
        proposals = proposals[:math.ceil(len(proposals) / 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)
 

	
 
    section = get_object_or_404(ProposalSection, section__slug=section_slug)
 

	
 
    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,
 
        "section": section,
 
    }
 
    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
 
@transaction.atomic
 
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 not admin and 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)
vendor/symposion/schedule/forms.py
Show inline comments
 
import csv
 
import time
 
from io import TextIOWrapper
 

	
 
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, ProposalBase)
 

	
 

	
 
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,
 
            "widget": forms.Textarea,
 
        }
 
        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'
 
    EXCLUSIVE = 'exclusive'
 
    PROPOSAL = 'proposal_id'
 
    KIND = 'kind'
 

	
 
    filename = forms.FileField(
 
        label='Select a CSV file to import:',
 
        required=False
 
    )
 

	
 
    def __init__(self, *args, **kwargs):
 
        self.schedule = kwargs.pop("schedule")
 
        if 'encoding' in kwargs:
 
            self.encoding = kwargs['encoding']
 
            kwargs.pop('encoding')
 
        else:
 
            self.encoding = "utf-8"
 
        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):
 
            try:
 
                room = Room.objects.get(schedule=self.schedule, name=room)
 
                created = False
 
            except Room.DoesNotExist:
 
                room = Room.objects.create(
 
                    schedule=self.schedule, name=room, order=i
 
                )
 
                created = True
 
            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, "%Y-%m-%d")
 
            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 = []
 
        f = TextIOWrapper(self.cleaned_data.get('filename'), encoding=self.encoding)
 
        reader = csv.DictReader(f)
 
        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], "%Y-%m-%d")
 
            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, exclusive=bool(int(row[self.EXCLUSIVE]))
 
                )
 
                if created:
 
                    created_items.append(slot)
 
            else:
 
                slot = Slot.objects.create(
 
                    kind=slot_kind, day=day, start=start, end=end, exclusive=bool(int(row[self.EXCLUSIVE]))
 
                )
 
                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.'
vendor/symposion/schedule/models.py
Show inline comments
 
import datetime
 

	
 
from django.core.exceptions import ObjectDoesNotExist
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from django.db import models
 
from django.utils.translation import ugettext_lazy as _
 

	
 
from symposion.managers import DefaultSelectRelatedManager
 
from symposion.text_parser import parse
 
from symposion.proposals.models import ProposalBase
 
from symposion.conference.models import Section
 
from symposion.speakers.models import Speaker
 

	
 
User = get_user_model()
 

	
 

	
 
class Schedule(models.Model):
 
    objects = DefaultSelectRelatedManager()
 

	
 
    section = models.OneToOneField(Section, verbose_name=_("Section"))
 
    section = models.OneToOneField(
 
        Section,
 
        verbose_name=_("Section"),
 
        on_delete=models.CASCADE,
 
    )
 
    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 SymposionMeta:
 
        select_related = ('section', )
 

	
 
    class Meta:
 
        ordering = ["section"]
 
        verbose_name = _('Schedule')
 
        verbose_name_plural = _('Schedules')
 

	
 

	
 
class Day(models.Model):
 
    objects = DefaultSelectRelatedManager()
 

	
 

	
 
    schedule = models.ForeignKey(Schedule, verbose_name=_("Schedule"))
 
    schedule = models.ForeignKey(
 
        Schedule,
 
        verbose_name=_("Schedule"),
 
        on_delete=models.CASCADE,
 
    )
 
    date = models.DateField(verbose_name=_("Date"))
 

	
 
    def __str__(self):
 
        return "%s: %s" % (self.schedule.section.name, self.date)
 

	
 
    class SymposionMeta:
 
        select_related = ('schedule__section', )
 

	
 
    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"))
 
    schedule = models.ForeignKey(
 
        Schedule,
 
        verbose_name=_("Schedule"),
 
        on_delete=models.CASCADE,
 
    )
 
    name = models.CharField(max_length=65, verbose_name=_("Name"))
 
    order = models.PositiveIntegerField(verbose_name=_("Order"))
 

	
 
    def __unicode__(self):
 
        return self.name
 

	
 
    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)
 
    room = models.ForeignKey(Room, on_delete=models.CASCADE)
 
    day = models.ForeignKey(Day, on_delete=models.CASCADE)
 

	
 
    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"))
 
    schedule = models.ForeignKey(
 
        Schedule,
 
        verbose_name=_("schedule"),
 
        on_delete=models.CASCADE,
 
    )
 
    label = models.CharField(max_length=50, verbose_name=_("Label"))
 

	
 
    def __str__(self):
 
        return ": ".join((self.schedule.section.name, self.label))
 

	
 
    class Meta:
 
        verbose_name = _("Slot kind")
 
        verbose_name_plural = _("Slot kinds")
 

	
 

	
 
class Slot(models.Model):
 
    objects = DefaultSelectRelatedManager()
 

	
 
    name = models.CharField(max_length=512, editable=False)
 
    day = models.ForeignKey(Day, verbose_name=_("Day"))
 
    kind = models.ForeignKey(SlotKind, verbose_name=_("Kind"))
 
    day = models.ForeignKey(
 
        Day,
 
        verbose_name=_("Day"),
 
        on_delete=models.CASCADE,
 
    )
 
    kind = models.ForeignKey(
 
        SlotKind,
 
        verbose_name=_("Kind"),
 
        on_delete=models.CASCADE,
 
    )
 
    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(str, self.rooms))
 
        self.name = "%s %s (%s - %s) %s" % (self.day.date, self.kind.label, self.start, self.end, roomlist)
 
        self.content_override_html = parse(self.content_override)
 
        super(Slot, self).save(*args, **kwargs)
 

	
 
    def __str__(self):
 
        return "%s: %s" % (self.day.schedule.section.name, self.name)
 

	
 
    class SymposionMeta:
 
        select_related = ('kind__schedule__section', 'day')
 

	
 
    class Meta:
 
        ordering = ["day__schedule__section__name", "day", "start" ]
 
        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"))
 
    slot = models.ForeignKey(
 
        Slot,
 
        verbose_name=_("Slot"),
 
        on_delete=models.CASCADE,
 
    )
 
    room = models.ForeignKey(
 
        Room,
 
        verbose_name=_("Room"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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):
 
    objects = DefaultSelectRelatedManager()
 

	
 
    slot = models.OneToOneField(Slot, null=True, blank=True, related_name="content_ptr", verbose_name=_("Slot"))
 
    slot = models.OneToOneField(
 
        Slot,
 
        null=True,
 
        blank=True,
 
        related_name="content_ptr",
 
        verbose_name=_("Slot"),
 
        on_delete=models.CASCADE,
 
    )
 
    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"))
 
    speaker = models.ForeignKey(
 
        Speaker,
 
        related_name="presentations",
 
        verbose_name=_("Speaker"),
 
        on_delete=models.CASCADE,
 
    )
 
    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"))
 
    proposal_base = models.OneToOneField(
 
        ProposalBase,
 
        related_name="presentation",
 
        verbose_name=_("Proposal base"),
 
        on_delete=models.CASCADE,
 
    )
 
    section = models.ForeignKey(
 
        Section,
 
        related_name="presentations",
 
        verbose_name=_("Section"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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"))
 
    day = models.ForeignKey(
 
        Day,
 
        related_name="sessions",
 
        verbose_name=_("Day"),
 
        on_delete=models.CASCADE,
 
    )
 
    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
 

	
 
    @property
 
    def rooms(self):
 
        return Room.objects.filter(slotroom__slot__in=self.slots.all()).distinct()
 

	
 
    @property
 
    def room_names(self):
 
        return ', '.join(room.name for room in self.rooms)
 

	
 
    def __str__(self):
 
        start = self.start()
 
        end = self.end()
 
        rooms = ', '.join(room.name for room in self.rooms)
 
        if start and end:
 
            return "%s: %s - %s in %s" % (
 
                self.day.date.strftime("%a"),
 
                start.strftime("%X"),
 
                end.strftime("%X"),
 
                rooms
 
            )
 
        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"))
 
    session = models.ForeignKey(
 
        Session,
 
        verbose_name=_("Session"),
 
        on_delete=models.CASCADE,
 
    )
 
    user = models.ForeignKey(
 
        User,
 
        verbose_name=_("User"),
 
        on_delete=models.CASCADE,
 
    )
 
    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])
vendor/symposion/schedule/tests/factories.py
Show inline comments
 
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):
 
class ConferenceFactory(factory.django.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):
 
class SectionFactory(factory.django.DjangoModelFactory):
 
    conference = factory.SubFactory(ConferenceFactory)
 
    name = fuzzy.FuzzyText()
 
    slug = fuzzy.FuzzyText()
 

	
 
    class Meta:
 
        model = Section
 

	
 

	
 
class ScheduleFactory(factory.DjangoModelFactory):
 
class ScheduleFactory(factory.django.DjangoModelFactory):
 
    section = factory.SubFactory(SectionFactory)
 
    published = True
 
    hidden = False
 

	
 
    class Meta:
 
        model = Schedule
 

	
 

	
 
class SlotKindFactory(factory.DjangoModelFactory):
 
class SlotKindFactory(factory.django.DjangoModelFactory):
 
    schedule = factory.SubFactory(ScheduleFactory)
 
    label = fuzzy.FuzzyText()
 

	
 
    class Meta:
 
        model = SlotKind
 

	
 

	
 
class DayFactory(factory.DjangoModelFactory):
 
class DayFactory(factory.django.DjangoModelFactory):
 
    schedule = factory.SubFactory(ScheduleFactory)
 
    date = fuzzy.FuzzyDate(datetime.date(2014, 1, 1))
 

	
 
    class Meta:
 
        model = Day
 

	
 

	
 
class SlotFactory(factory.DjangoModelFactory):
 
class SlotFactory(factory.django.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
vendor/symposion/schedule/tests/test_views_session.py
Show inline comments
 
from datetime import date
 

	
 
from django.conf import settings
 
from django.contrib.auth.models import User
 
from django.core.urlresolvers import reverse
 
from django.contrib.auth import get_user_model
 
from django.urls import reverse
 
from django.test import TestCase
 

	
 
from symposion.conference.models import Section, current_conference, Conference
 
from symposion.schedule.models import Day, Schedule, Session
 

	
 
User = get_user_model()
 

	
 

	
 
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)
vendor/symposion/schedule/views.py
Show inline comments
 
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.urls import reverse
 
from django.template import loader, Context
 
from django.conf import settings
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
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
 
from pinaxcon.templatetags.lca2018_tags import speaker_photo
 

	
 
User = get_user_model()
 

	
 

	
 
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,
 
        })
 

	
 
    ctx = {
 
        "sections": sections,
 
    }
 
    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
 

	
...
 
@@ -276,145 +278,145 @@ def schedule_json(request):
 
            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():
 
        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():
 
        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 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,
 
    })
vendor/symposion/speakers/models.py
Show inline comments
 
import datetime
 

	
 
from django.core.urlresolvers import reverse
 
from django.db import models
 
from django.urls import reverse
 
from django.utils.translation import ugettext_lazy as _
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 

	
 
from symposion import constants
 
from symposion.text_parser import parse
 

	
 
User = get_user_model()
 

	
 

	
 
class Speaker(models.Model):
 

	
 
    user = models.OneToOneField(User, null=True, related_name="speaker_profile", verbose_name=_("User"))
 
    user = models.OneToOneField(
 
        User,
 
        null=True,
 
        related_name="speaker_profile",
 
        verbose_name=_("User"),
 
        on_delete=models.CASCADE,
 
    )
 
    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=_("Let us know how we can help you during the conference, for example "
 
                    "your accessibility requirements, whether you require access to child "
 
                    "minding facilities, or anything else you think we should know about."),
 
        verbose_name=_("Other 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 linux.conf.au "
 
                    "in order 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 accommodation "
 
                    "in order to present your proposed sessions."),
 
        verbose_name=_("Accommodation assistance required"),
 
    )
 
    assistance = models.TextField(
 
        blank=True,
 
        help_text=_("We have budget set aside to provide financial assistance to "
 
                    "linux.conf.au speakers and attendees who might otherwise find it difficult to attend. "
 
                    "Please provide details on why you require travel and/or accommodation assistance "
 
                    "in order to present your proposed sessions. "
 
                    "For travel assistance, please also tell us where you will be coming from "
 
                    "(country, state, etc) to assist with planning."),
 
        verbose_name=_("Travel/Accommodation assistance details"))
 
    assistance_html = models.TextField(blank=True)
 
    agreement = models.BooleanField(
 
        default=False,
 
        help_text=_("I agree to the "
 
                    "<a href=\"/attend/terms-and-conditions\"> "
 
                    "terms and conditions of attendance</a>, and I have read, "
 
                    "understood, and agree to act according to the standards set "
 
                    "forth in our "
 
                    "<a href=\"/attend/code-of-conduct\">"
 
                    "Code of Conduct</a>.")
 
    )
 

	
 
    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):
vendor/symposion/speakers/views.py
Show inline comments
 
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.contrib.auth import get_user_model
 
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
 

	
 
User = get_user_model()
 

	
 

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

	
vendor/symposion/sponsorship/models.py
Show inline comments
 
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.urls import reverse
 
from django.utils.translation import ugettext_lazy as _
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 

	
 
from symposion.conference.models import Conference
 
from symposion.sponsorship.managers import SponsorManager
 

	
 
User = get_user_model()
 

	
 

	
 
# 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"))
 
    conference = models.ForeignKey(
 
        Conference,
 
        verbose_name=_("Conference"),
 
        on_delete=models.CASCADE,
 
    )
 
    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)
 
    applicant = models.ForeignKey(
 
        User,
 
        related_name="sponsorships",
 
        verbose_name=_("Applicant"),
 
        null=True,
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    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"))
 
    level = models.ForeignKey(
 
        SponsorLevel,
 
        verbose_name=_("level"),
 
        on_delete=models.CASCADE,
 
    )
 
    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"))
 
    sponsor_logo = models.ForeignKey(
 
        "SponsorBenefit",
 
        related_name="+",
 
        null=True,
 
        blank=True,
 
        editable=False,
 
        verbose_name=_("Sponsor logo"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    # 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"))
 
    benefit = models.ForeignKey(
 
        Benefit,
 
        related_name="benefit_levels",
 
        verbose_name=_("Benefit"),
 
        on_delete=models.CASCADE,
 
    )
 
    level = models.ForeignKey(
 
        SponsorLevel,
 
        related_name="benefit_levels",
 
        verbose_name=_("Level"),
 
        on_delete=models.CASCADE,
 
    )
 

	
 
    # 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"))
 
    sponsor = models.ForeignKey(
 
        Sponsor,
 
        related_name="sponsor_benefits",
 
        verbose_name=_("Sponsor"),
 
        on_delete=models.CASCADE,
 
    )
 
    benefit = models.ForeignKey(
 
        Benefit,
 
        related_name="sponsor_benefits",
 
        verbose_name=_("Benefit"),
 
        on_delete=models.CASCADE,
 
    )
 
    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)
vendor/symposion/sponsorship/tests.py
Show inline comments
 
from cStringIO import StringIO
 
from io import StringIO
 
import os
 
import shutil
 
import tempfile
 
from zipfile import ZipFile
 

	
 
from django.conf import settings
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 
from django.core.exceptions import ValidationError
 
from django.core.urlresolvers import reverse
 
from django.test import TestCase
 
from django.test.utils import override_settings
 
from django.urls import reverse
 

	
 
from pycon.sponsorship.models import Benefit, Sponsor, SponsorBenefit,\
 
    SponsorLevel
 
from symposion.conference.models import current_conference
 

	
 
User = get_user_model()
 

	
 

	
 
class TestSponsorZipDownload(TestCase):
 
    def setUp(self):
 
        self.user = User.objects.create_user(username='joe',
 
                                             email='joe@example.com',
 
                                             password='joe')
 
        self.user.is_staff = True
 
        self.user.save()
 
        self.url = reverse("sponsor_zip_logos")
 
        self.assertTrue(self.client.login(username='joe@example.com',
 
                                          password='joe'))
 

	
 
        # we need a sponsor
 
        conference = current_conference()
 
        self.sponsor_level = SponsorLevel.objects.create(
 
            conference=conference, name="Lead", cost=1)
 
        self.sponsor = Sponsor.objects.create(
 
            name="Big Daddy",
 
            level=self.sponsor_level,
 
            active=True,
 
        )
 

	
 
        # Create our benefits, of various types
 
        self.text_benefit = Benefit.objects.create(name="text", type="text")
 
        self.file_benefit = Benefit.objects.create(name="file", type="file")
 
        # These names must be spelled exactly this way:
 
        self.weblogo_benefit = Benefit.objects.create(name="Web logo", type="weblogo")
 
        self.printlogo_benefit = Benefit.objects.create(name="Print logo", type="file")
 
        self.advertisement_benefit = Benefit.objects.create(name="Advertisement", type="file")
 

	
 
    def validate_response(self, rsp, names_and_sizes):
 
        # Ensure a response from the view looks right, contains a valid
 
        # zip archive, has files with the right names and sizes.
 
        self.assertEqual("application/zip", rsp['Content-type'])
 
        prefix = settings.CONFERENCE_URL_PREFIXES[settings.CONFERENCE_ID]
 

	
 
        self.assertEqual(
 
            'attachment; filename="pycon_%s_sponsorlogos.zip"' % prefix,
 
            rsp['Content-Disposition'])
 
        zipfile = ZipFile(StringIO(rsp.content), "r")
 
        # Check out the zip - testzip() returns None if no errors found
 
        self.assertIsNone(zipfile.testzip())
 
        # Compare contents to what is expected
 
        infolist = zipfile.infolist()
 
        self.assertEqual(len(names_and_sizes), len(infolist))
 
        for info, name_and_size in zip(infolist, names_and_sizes):
 
            name, size = name_and_size
 
            self.assertEqual(name, info.filename)
 
            self.assertEqual(size, info.file_size)
 

	
 
    def make_temp_file(self, name, size=0):
 
        # Create a temp file with the given name and size under self.temp_dir
 
        path = os.path.join(self.temp_dir, name)
 
        with open(path, "wb") as f:
 
            f.write(size * "x")
 

	
 
    def test_must_be_logged_in(self):
 
        # Must be logged in to use the view
 
        # If not logged in, doesn't redirect, just serves up a login view
 
        self.client.logout()
 
        rsp = self.client.get(self.url)
 
        self.assertEqual(200, rsp.status_code)
 
        self.assertIn("""<body class="login">""", rsp.content)
 

	
 
    def test_must_be_staff(self):
 
        # Only staff can use the view
 
        # If not staff, doesn't show error, just serves up a login view
 
        # Also, the dashboard doesn't show the download button
 
        self.user.is_staff = False
 
        self.user.save()
 
        rsp = self.client.get(self.url)
 
        self.assertEqual(200, rsp.status_code)
 
        self.assertIn("""<body class="login">""", rsp.content)
 
        rsp = self.client.get(reverse('dashboard'))
 
        self.assertNotIn(self.url, rsp.content)
 

	
 
    def test_no_files(self):
 
        # If there are no sponsor files, we still work
 
        # And the dashboard shows our download button
 
        rsp = self.client.get(self.url)
 
        self.validate_response(rsp, [])
 
        rsp = self.client.get(reverse('dashboard'))
 
        self.assertIn(self.url, rsp.content)
 

	
 
    def test_different_benefit_types(self):
 
        # We only get files from the benefits named "Print logo" and "Web logo"
 
        # And we ignore any non-existent files
 
        try:
 
            # Create a temp dir for media files
 
            self.temp_dir = tempfile.mkdtemp()
 
            with override_settings(MEDIA_ROOT=self.temp_dir):
 

	
 
                # Give our sponsor some benefits
 
                SponsorBenefit.objects.create(
 
                    sponsor=self.sponsor,
 
                    benefit=self.text_benefit,
vendor/symposion/teams/backends.py
Show inline comments
 
from django.db.models import Q
 

	
 
from .models import Team
 

	
 

	
 
class TeamPermissionsBackend(object):
 

	
 
    def authenticate(self, username=None, password=None):
 
    def authenticate(self, request, username=None, password=None):
 
        return None
 

	
 
    def get_team_permissions(self, user_obj, obj=None):
 
        """
 
        Returns a set of permission strings that this user has through his/her
 
        team memberships.
 
        """
 
        if user_obj.is_anonymous() or obj is not None:
 
        if user_obj.is_anonymous or obj is not None:
 
            return set()
 
        if not hasattr(user_obj, "_team_perm_cache"):
 
            # Member permissions
 
            memberships = Team.objects.filter(
 
                Q(memberships__user=user_obj),
 
                Q(memberships__state="member"),
 
            )
 
            perms = memberships.values_list(
 
                "permissions__content_type__app_label",
 
                "permissions__codename"
 
            ).order_by()
 
            permissions = ["%s.%s" % (ct, name) for ct, name in perms]
 
            # Manager permissions
 
            memberships = Team.objects.filter(
 
                Q(memberships__user=user_obj),
 
                Q(memberships__state="manager"),
 
            )
 
            perms = memberships.values_list(
 
                "manager_permissions__content_type__app_label",
 
                "manager_permissions__codename"
 
            ).order_by()
 
            permissions += ["%s.%s" % (ct, name) for ct, name in perms]
 
            user_obj._team_perm_cache = set(permissions)
 
        return user_obj._team_perm_cache
 

	
 
    def has_perm(self, user_obj, perm, obj=None):
 
        if not user_obj.is_active:
 
            return False
 
        return perm in self.get_team_permissions(user_obj, obj)
vendor/symposion/teams/forms.py
Show inline comments
 
from django import forms
 

	
 
from django.utils.html import escape
 
from django.utils.safestring import mark_safe
 
from django.utils.translation import ugettext_lazy as _
 

	
 
from django.contrib.auth.models import User
 
from django.contrib.auth import get_user_model
 

	
 
from symposion.teams.models import Membership
 

	
 
User = get_user_model()
 

	
 

	
 
class TeamInvitationForm(forms.Form):
 

	
 
    required_css_class = 'label-required'
 

	
 
    email = forms.EmailField(label=_("Email"),
 
                             help_text=_("email address must be that of an account on this "
 
                                         "conference site"))
 

	
 
    def __init__(self, *args, **kwargs):
 
        self.team = kwargs.pop("team")
 
        super(TeamInvitationForm, self).__init__(*args, **kwargs)
 

	
 
    def clean(self):
 
        cleaned_data = super(TeamInvitationForm, self).clean()
 
        email = cleaned_data.get("email")
 

	
 
        if email is None:
 
            raise forms.ValidationError(_("valid email address required"))
 

	
 
        try:
 
            user = User.objects.get(email=email)
 
        except User.DoesNotExist:
 
            # eventually we can invite them but for now assume they are
 
            # already on the site
 
            raise forms.ValidationError(
 
                mark_safe(_("no account with email address <b>%s</b> found on this conference "
 
                          "site") % escape(email)))
 

	
 
        state = self.team.get_state_for_user(user)
 

	
 
        if state in ["member", "manager"]:
 
            raise forms.ValidationError(_("user already in team"))
 

	
 
        if state in ["invited"]:
 
            raise forms.ValidationError(_("user already invited to team"))
 

	
 
        self.user = user
 
        self.state = state
 

	
 
        return cleaned_data
 

	
 
    def invite(self):
 
        if self.state is None:
 
            Membership.objects.create(team=self.team, user=self.user, state="invited")
 
        elif self.state == "applied":
 
            # if they applied we shortcut invitation process
 
            membership = Membership.objects.filter(team=self.team, user=self.user)
 
            membership.update(state="member")
vendor/symposion/teams/migrations/0001_initial.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
from django.db import models, migrations
 
import datetime
 
from django.conf import settings
 
import django.db.models.deletion
 

	
 

	
 
class Migration(migrations.Migration):
 

	
 
    dependencies = [
 
        ('auth', '0006_require_contenttypes_0002'),
 
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
 
    ]
 

	
 
    operations = [
 
        migrations.CreateModel(
 
            name='Membership',
 
            fields=[
 
                ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
 
                ('state', models.CharField(max_length=20, choices=[('applied', 'applied'), ('invited', 'invited'), ('declined', 'declined'), ('rejected', 'rejected'), ('member', 'member'), ('manager', 'manager')], verbose_name='State')),
 
                ('message', models.TextField(blank=True, verbose_name='Message')),
 
            ],
 
            options={
 
                'verbose_name_plural': 'Memberships',
 
                'verbose_name': 'Membership',
 
            },
 
        ),
 
        migrations.CreateModel(
 
            name='Team',
 
            fields=[
 
                ('id', models.AutoField(serialize=False, auto_created=True, primary_key=True, verbose_name='ID')),
 
                ('slug', models.SlugField(unique=True, verbose_name='Slug')),
 
                ('name', models.CharField(max_length=100, verbose_name='Name')),
 
                ('description', models.TextField(blank=True, verbose_name='Description')),
 
                ('access', models.CharField(max_length=20, choices=[('open', 'open'), ('application', 'by application'), ('invitation', 'by invitation')], verbose_name='Access')),
 
                ('created', models.DateTimeField(editable=False, default=datetime.datetime.now, verbose_name='Created')),
 
                ('manager_permissions', models.ManyToManyField(related_name='manager_teams', blank=True, to='auth.Permission', verbose_name='Manager permissions')),
 
                ('permissions', models.ManyToManyField(related_name='member_teams', blank=True, to='auth.Permission', verbose_name='Permissions')),
 
            ],
 
            options={
 
                'verbose_name_plural': 'Teams',
 
                'verbose_name': 'Team',
 
            },
 
        ),
 
        migrations.AddField(
 
            model_name='membership',
 
            name='team',
 
            field=models.ForeignKey(verbose_name='Team', to='teams.Team', related_name='memberships'),
 
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, verbose_name='Team', to='teams.Team', related_name='memberships'),
 
        ),
 
        migrations.AddField(
 
            model_name='membership',
 
            name='user',
 
            field=models.ForeignKey(verbose_name='User', to=settings.AUTH_USER_MODEL, related_name='memberships'),
 
            field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, verbose_name='User', to=settings.AUTH_USER_MODEL, related_name='memberships'),
 
        ),
 
    ]
vendor/symposion/teams/models.py
Show inline comments
 
import datetime
 

	
 
from django.db import models
 
from django.utils.translation import ugettext_lazy as _
 
from django.urls import reverse
 

	
 
from django.contrib.auth.models import Permission, User
 
from django.contrib.auth.models import Permission
 
from django.contrib.auth import get_user_model
 

	
 
from reversion import revisions as reversion
 

	
 
User = get_user_model()
 

	
 

	
 
TEAM_ACCESS_CHOICES = [
 
    ("open", _("open")),
 
    ("application", _("by application")),
 
    ("invitation", _("by invitation"))
 
]
 

	
 

	
 
class Team(models.Model):
 

	
 
    slug = models.SlugField(unique=True, verbose_name=_("Slug"))
 
    name = models.CharField(max_length=100, verbose_name=_("Name"))
 
    description = models.TextField(blank=True, verbose_name=_("Description"))
 
    access = models.CharField(max_length=20, choices=TEAM_ACCESS_CHOICES,
 
                              verbose_name=_("Access"))
 

	
 
    # member permissions
 
    permissions = models.ManyToManyField(Permission, blank=True,
 
                                         related_name="member_teams",
 
                                         verbose_name=_("Permissions"))
 

	
 
    # manager permissions
 
    manager_permissions = models.ManyToManyField(Permission, blank=True,
 
                                                 related_name="manager_teams",
 
                                                 verbose_name=_("Manager permissions"))
 

	
 
    created = models.DateTimeField(default=datetime.datetime.now,
 
                                   editable=False, verbose_name=_("Created"))
 

	
 
    @models.permalink
 
    def get_absolute_url(self):
 
        return ("team_detail", [self.slug])
 
        return reverse("team_detail", args=[self.slug])
 

	
 
    def __str__(self):
 
        return self.name
 

	
 
    def get_state_for_user(self, user):
 
        try:
 
            return self.memberships.get(user=user).state
 
        except Membership.DoesNotExist:
 
            return None
 

	
 
    def applicants(self):
 
        return self.memberships.filter(state="applied")
 

	
 
    def invitees(self):
 
        return self.memberships.filter(state="invited")
 

	
 
    def members(self):
 
        return self.memberships.filter(state="member")
 

	
 
    def managers(self):
 
        return self.memberships.filter(state="manager")
 

	
 
    class Meta:
 
        verbose_name = _('Team')
 
        verbose_name_plural = _('Teams')
 

	
 

	
 
MEMBERSHIP_STATE_CHOICES = [
 
    ("applied", _("applied")),
 
    ("invited", _("invited")),
 
    ("declined", _("declined")),
 
    ("rejected", _("rejected")),
 
    ("member", _("member")),
 
    ("manager", _("manager")),
 
]
 

	
 

	
 
class Membership(models.Model):
 

	
 
    user = models.ForeignKey(User, related_name="memberships",
 
                             verbose_name=_("User"))
 
    team = models.ForeignKey(Team, related_name="memberships",
 
                             verbose_name=_("Team"))
 
    user = models.ForeignKey(
 
        User,
 
        related_name="memberships",
 
        verbose_name=_("User"),
 
        on_delete=models.CASCADE,
 
    )
 
    team = models.ForeignKey(
 
        Team,
 
        related_name="memberships",
 
        verbose_name=_("Team"),
 
        on_delete=models.CASCADE,
 
    )
 
    state = models.CharField(max_length=20, choices=MEMBERSHIP_STATE_CHOICES,
 
                             verbose_name=_("State"))
 
    message = models.TextField(blank=True, verbose_name=_("Message"))
 

	
 
    class Meta:
 
        verbose_name = _("Membership")
 
        verbose_name_plural = _("Memberships")
 

	
 

	
 
reversion.register(Membership)
vendor/symposion/utils/mail.py
Show inline comments
 
import os
 

	
 
from django.conf import settings
 
from django.core.mail import EmailMultiAlternatives
 
from django.template.loader import render_to_string
 
from django.utils.html import strip_tags
 

	
 
from django.contrib.sites.models import Site
 

	
 

	
 
class Sender(object):
 
    ''' Class for sending e-mails under a templete prefix. '''
 
    ''' Class for sending e-mails under a template prefix. '''
 

	
 
    def __init__(self, template_prefix):
 
        self.template_prefix = template_prefix
 

	
 
    def send_email(self, to, kind, **kwargs):
 
        ''' Sends an e-mail to the given address.
 

	
 
        to: The address
 
        kind: the ID for an e-mail kind; it should point to a subdirectory of
 
            self.template_prefix containing subject.txt and message.html, which
 
            are django templates for the subject and HTML message respectively.
 

	
 
        context: a context for rendering the e-mail.
 

	
 
        '''
 

	
 
        return __send_email__(self.template_prefix, to, kind, **kwargs)
 

	
 

	
 
send_email = Sender("symposion/emails").send_email
 

	
 

	
 
def __send_email__(template_prefix, to, kind, **kwargs):
 

	
 
    current_site = Site.objects.get_current()
 

	
 
    ctx = {
 
        "current_site": current_site,
 
        "STATIC_URL": settings.STATIC_URL,
 
    }
 
    ctx.update(kwargs.get("context", {}))
 
    subject_template = os.path.join(template_prefix, "%s/subject.txt" % kind)
 
    message_template = os.path.join(template_prefix, "%s/message.html" % kind)
 
    subject = "[%s] %s" % (
 
        current_site.name,
 
        render_to_string(subject_template, ctx).strip()
 
    )
 

	
 
    message_html = render_to_string(message_template, ctx)
 
    message_plaintext = strip_tags(message_html)
 

	
 
    from_email = settings.DEFAULT_FROM_EMAIL
 

	
 
    email = EmailMultiAlternatives(subject, message_plaintext, from_email, to)
 
    email.attach_alternative(message_html, "text/html")
 
    email.send()
0 comments (0 inline, 0 general)