PK(HV wafer/urls.pyfrom django.conf.urls import include, patterns, url from django.conf.urls.static import static from django.conf import settings from django.contrib import admin admin.autodiscover() urlpatterns = patterns( '', (r'^accounts/', include('wafer.registration.urls')), (r'^users/', include('wafer.users.urls')), (r'^talks/', include('wafer.talks.urls')), (r'^sponsors/', include('wafer.sponsors.urls')), (r'^pages/', include('wafer.pages.urls')), (r'^admin/', include(admin.site.urls)), (r'^markitup/', include('markitup.urls')), (r'^schedule/', include('wafer.schedule.urls')), (r'^tickets/', include('wafer.tickets.urls')), (r'^kv/', include('wafer.kv.urls')), ) # Serve media if settings.DEBUG: urlpatterns += static( settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # Pages occupy the entire URL space, and must come last urlpatterns.append(url(r'', include('wafer.pages.urls'))) PK}H Ivxxwafer/context_processors.pyfrom django.conf import settings from django.contrib.sites.shortcuts import get_current_site from wafer.menu import get_cached_menus def site_info(request): '''Expose the site's info to templates''' site = get_current_site(request) context = { 'WAFER_CONFERENCE_NAME': site.name, 'WAFER_CONFERENCE_DOMAIN': site.domain, } return context def menu_info(request): '''Expose the menus to templates''' menus = get_cached_menus() context = { 'WAFER_MENUS': menus, } return context def registration_settings(request): '''Expose selected settings to templates''' context = {} for setting in ( 'WAFER_SSO', 'WAFER_HIDE_LOGIN', 'WAFER_REGISTRATION_OPEN', 'WAFER_REGISTRATION_MODE', ): context[setting] = getattr(settings, setting, None) return context PKOIG},[ wafer/menu.pyimport copy from django.core.cache import cache from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.utils import six CACHE_KEY = "WAFER_MENU_CACHE" def get_cached_menus(): """Return the menus from the cache or generate them if needed.""" items = cache.get(CACHE_KEY) if items is None: menu = generate_menu() cache.set(CACHE_KEY, menu.items) else: menu = Menu(items) return menu def clear_menu_cache(): """Clear the cached version of the menu (if any).""" cache.delete(CACHE_KEY) def refresh_menu_cache(**kwargs): """Refresh the menu cache. Takes **kwargs to make it easier to use as a Django signal handler. """ try: clear_menu_cache() get_cached_menus() except ObjectDoesNotExist as e: # During data loads, treat this as non-fatal, since we'll come back # here again with hopefully all the stuff required loaded eventually # (we use ObjectDoesNotExist to avoid awkard circular import issues) if not kwargs.get('raw'): raise e def maybe_obj(str_or_obj): """If argument is not a string, return it. Otherwise import the dotted name and return that. """ if not isinstance(str_or_obj, six.string_types): return str_or_obj parts = str_or_obj.split(".") mod, modname = None, None for p in parts: modname = p if modname is None else "%s.%s" % (modname, p) try: mod = __import__(modname) except ImportError: if mod is None: raise break obj = mod for p in parts[1:]: obj = getattr(obj, p) return obj def generate_menu(): """Generate a new list of menus.""" root_menu = Menu(list(copy.deepcopy(settings.WAFER_MENUS))) for dynamic_menu_func in settings.WAFER_DYNAMIC_MENUS: dynamic_menu_func = maybe_obj(dynamic_menu_func) dynamic_menu_func(root_menu) return root_menu class MenuError(Exception): """Raised when attempting illegal operations while constructiong menus.""" class Menu(object): """Utility class for manipulating a hierarchy of menus. A menu is maintained as a list of dictionaries (for ease of caching). Menu items are dictionaries with the keys: name, label and url. E.g.:: {"name": "home", "label": _("Home"), "url": reverse("wafer_page", args=('index',))}, Sub-menus are dictionaries with the keys: name, label and items. {"name": "sponsors", "label": _("Sponsors"), "items": [ {"name": "sponsors", "label": _("Our sponsors"), "url": reverse("wafer_sponsors")}, {"name": "packages", "label": _("Sponsorship packages"), "url": reverse("wafer_sponsorship_packages")}, ]}, Image button items can be adding an "image" key to the item. The label for the icon will be used as the alt-text for the image. {"name": "twitter", "label": _("Twitter"), "image": "/static/twitter.png", "url": "http://twitter.com/wafer"}, """ def __init__(self, items): self.items = items @staticmethod def mk_item(label, url, sort_key=None, image=None): return {"label": label, "url": url, "sort_key": sort_key, "image": image} @staticmethod def mk_menu(name, label, items, sort_key=None): return {"name": name, "label": label, "items": items, "sort_key": sort_key} def _descend_items(self, menu): menu_items = self.items if menu is not None: matches = [item for item in menu_items if "items" in item and item.get("menu") == menu] if len(matches) != 1: raise MenuError("Unable to find sub-menu %r." % (menu,)) menu_items = matches[0]["items"] return menu_items def add_item(self, label, url, menu=None, sort_key=None, image=None): menu_items = self._descend_items(menu) menu_items.append(self.mk_item(label, url, sort_key=sort_key, image=image)) def add_menu(self, name, label, items, sort_key=None): self.items.append(self.mk_menu(name, label, items, sort_key=sort_key)) PK(HSLwafer/utils.pyimport functools import unicodedata from django.core.cache import caches from django.conf import settings from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator def normalize_unicode(u): """Replace non-ASCII characters with closest ASCII equivalents where possible. """ return unicodedata.normalize('NFKD', u).encode('ascii', 'ignore') def cache_result(cache_key, timeout): """A decorator for caching the result of a function.""" def decorator(f): cache_name = settings.WAFER_CACHE @functools.wraps(f) def wrapper(*args, **kw): # replace this with cache.caches when we drop Django 1.6 # compatibility cache = caches[cache_name] result = cache.get(cache_key) if result is None: result = f(*args, **kw) cache.set(cache_key, result, timeout) return result def invalidate(): cache = caches[cache_name] cache.delete(cache_key) wrapper.invalidate = invalidate return wrapper return decorator class QueryTracker(object): """ Track queries to database. """ def __enter__(self): from django.conf import settings from django.db import connection self._debug = settings.DEBUG settings.DEBUG = True del connection.queries[:] return self def __exit__(self, *args, **kw): from django.conf import settings settings.DEBUG = self._debug @property def queries(self): from django.db import connection return connection.queries[:] # XXX: Should we use Django's version for Django >= 1.9 ? # This should certainly go away when we drop support for # Django 1.8 class LoginRequiredMixin(object): '''Must be logged in''' @method_decorator(login_required) def dispatch(self, *args, **kwargs): return super(LoginRequiredMixin, self).dispatch(*args, **kwargs) PKyI.,]]wafer/__init__.py"""Wafer, a light-weight conference management library for Django.""" __version__ = "0.4.0" PKOIGlB wafer/wsgi.py""" WSGI config for wafer project. This module contains the WSGI application used by Django's development server and any production WSGI deployments. It should expose a module-level variable named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover this application via the ``WSGI_APPLICATION`` setting. Usually you will have the standard Django WSGI application here, but it also might make sense to replace the whole Django WSGI application with a custom one that later delegates to the Django one. For example, you could introduce WSGI middleware here, or combine a Django application with an application of another framework. """ import os # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # if running multiple sites in the same mod_wsgi process. To fix this, use # mod_wsgi daemon mode with each site in its own daemon process, or use # os.environ["DJANGO_SETTINGS_MODULE"] = "wafer.settings" os.environ.setdefault("DJANGO_SETTINGS_MODULE", "wafer.settings") # This application object is used by any WSGI server configured to use this # file. This includes Django's development server, if the WSGI_APPLICATION # setting points here. from django.core.wsgi import get_wsgi_application application = get_wsgi_application() # Apply WSGI middleware here. # from helloworld.wsgi import HelloWorldApplication # application = HelloWorldApplication(application) PKyIY##wafer/settings.pyimport os from django.utils.translation import ugettext_lazy as _ try: from localsettings import * except ImportError: pass # Django settings for wafer project. ADMINS = ( # The logging config below mails admins # ('Your Name', 'your_email@example.com'), ) DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': 'wafer.db', } } if os.environ.get('TESTDB', None) == 'postgres': DATABASES['default'].update({ 'ENGINE': 'django.db.backends.postgresql_psycopg2', 'USER': 'postgres', 'NAME': 'wafer', }) # Hosts/domain names that are valid for this site; required if DEBUG is False # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts ALLOWED_HOSTS = [] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # In a Windows environment this must be set to your system time zone. TIME_ZONE = 'UTC' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True # If you set this to False, Django will not format dates, numbers and # calendars according to the current locale. USE_L10N = True # If you set this to False, Django will not use timezone-aware datetimes. USE_TZ = True # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/var/www/example.com/media/" project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) MEDIA_ROOT = os.path.join(project_root, 'media') # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. # Examples: "http://example.com/media/", "http://media.example.com/" MEDIA_URL = '/media/' # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/var/www/example.com/static/" STATIC_ROOT = '' # URL prefix for static files. # Example: "http://example.com/static/", "http://static.example.com/" STATIC_URL = '/static/' # Additional locations of static files STATICFILES_DIRS = ( # Put strings here, like "/home/html/static" or "C:/www/django/static". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. os.path.join(project_root, 'bower_components'), ) # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 'django.contrib.staticfiles.finders.DefaultStorageFinder', ) # Make this unique, and don't share it with anybody. SECRET_KEY = '8iysa30^no&oi5kv$k1w)#gsxzrylr-h6%)loz71expnbf7z%)' # List of callables that know how to import templates from various sources. TEMPLATE_LOADERS = ( 'django.template.loaders.filesystem.Loader', 'django.template.loaders.app_directories.Loader', # 'django.template.loaders.eggs.Loader', ) MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) ROOT_URLCONF = 'wafer.urls' # Python dotted path to the WSGI application used by Django's runserver. WSGI_APPLICATION = 'wafer.wsgi.application' TEMPLATE_DIRS = ( # Put strings here, like "/home/html/django_templates" or # "C:/www/django/templates". Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. ) TEMPLATE_CONTEXT_PROCESSORS = ( 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.debug', 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.static', 'django.core.context_processors.tz', 'django.contrib.messages.context_processors.messages', 'wafer.context_processors.site_info', 'wafer.context_processors.menu_info', 'wafer.context_processors.registration_settings', ) INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', 'reversion', 'django_medusa', 'crispy_forms', 'django_nose', 'markitup', 'rest_framework', 'easy_select2', 'wafer', 'wafer.kv', 'wafer.registration', 'wafer.talks', 'wafer.schedule', 'wafer.users', 'wafer.sponsors', 'wafer.pages', 'wafer.tickets', 'wafer.compare', # Django isn't finding the overridden templates 'registration', 'django.contrib.admin', ) TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' } }, 'handlers': { 'mail_admins': { 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' } }, 'loggers': { 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': True, }, } } # Django registration: ACCOUNT_ACTIVATION_DAYS = 7 AUTH_USER_MODEL = 'auth.User' # Forms: CRISPY_TEMPLATE_PACK = 'bootstrap3' # Wafer cache settings # We assume that the WAFER_CACHE is cross-process WAFER_CACHE = 'wafer_cache' CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', }, WAFER_CACHE: { 'BACKEND': 'django.core.cache.backends.db.DatabaseCache', 'LOCATION': 'wafer_cache_table', }, } # Wafer menu settings WAFER_MENUS = () # Example menus entries: # # {"label": _("Home"), # "url": '/'}, # {"menu": "sponsors", # "label": _("Sponsors"), # "items": [ # {"name": "sponsors", "label": _("Our sponsors"), # "url": reverse_lazy("wafer_sponsors")}, # {"name": "packages", "label": _("Sponsorship packages"), # "url": reverse_lazy("wafer_sponsorship_packages")}, # ]}, # {"label": _("Talks"), # "url": reverse_lazy("wafer_users_talks")}, WAFER_DYNAMIC_MENUS = ( 'wafer.pages.models.page_menus', ) # Enabled SSO mechanims: WAFER_SSO = ( # 'github', # 'debian', ) # Log in with GitHub: # WAFER_GITHUB_CLIENT_ID = 'register on github' # WAFER_GITHUB_CLIENT_SECRET = 'to get these secrets' # Log in with Debian SSO: # Requires some Apache config: # SSLCACertificateFile /srv/sso.debian.org/etc/debsso.crt # SSLCARevocationCheck chain # SSLCARevocationFile /srv/sso.debian.org/etc/debsso.crl # # SSLOptions +StdEnvVars # SSLVerifyClient optional # # WAFER_DEBIAN_NM_API_KEY = 'obtain one from https://nm.debian.org/apikeys/' # Set this to true to disable the login button on the navigation toolbar WAFER_HIDE_LOGIN = False # Talk submissions open # Set this to False to disable talk submissions WAFER_TALKS_OPEN = True # The form used for talk submission WAFER_TALK_FORM = 'wafer.talks.forms.TalkForm' # Set this to False to disable registration WAFER_REGISTRATION_OPEN = True # Can be 'ticket' for Quicket tickets or 'form' for a classic form WAFER_REGISTRATION_MODE = 'ticket' # For REGISTRATION_MODE == 'form', the form to present WAFER_REGISTRATION_FORM = 'wafer.users.forms.ExampleRegistrationForm' # Allow registered and anonymous users to see registered users WAFER_PUBLIC_ATTENDEE_LIST = True # Ticket registration with Quicket # WAFER_TICKET_SECRET = "i'm a shared secret" # django_medusa -- disk-based renderer MEDUSA_RENDERER_CLASS = "wafer.management.static.WaferDiskStaticSiteRenderer" MEDUSA_DEPLOY_DIR = os.path.join(project_root, 'static_mirror') MARKITUP_FILTER = ('markdown.markdown', {'safe_mode': True}) REST_FRAMEWORK = { 'DEFAULT_PERMISSION_CLASSES': ('rest_framework.permissions.IsAdminUser',), 'PAGE_SIZE': 50 } PKv|GKWWwafer/registration/urls.pyfrom django.conf.urls import include, patterns, url from django.views.generic import TemplateView from registration.backends.default.views import ActivationView, RegistrationView urlpatterns = patterns( 'wafer.registration.views', url(r'^profile/$', 'redirect_profile'), url(r'^github-login/$', 'github_login'), url(r'^debian-login/$', 'debian_login'), # registration.backends.default.urls, but Django 1.5 compatible url(r'^activate/complete/$', TemplateView.as_view( template_name='registration/activation_complete.html' ), name='registration_activation_complete'), url(r'^activate/(?P\w+)/$', ActivationView.as_view( template_name='registration/activate.html' ), name='registration_activate'), url(r'^register/$', RegistrationView.as_view( template_name='registration/registration_form.html' ), name='registration_register'), url(r'^register/complete/$', TemplateView.as_view( template_name='registration/registration_complete.html' ), name='registration_complete'), url(r'^register/closed/$', TemplateView.as_view( template_name='registration/registration_closed.html' ), name='registration_disallowed'), url(r'', include('registration.auth_urls')), ) PK}H6vwafer/registration/views.pyimport urllib from django.contrib.auth import login from django.contrib.auth.views import redirect_to_login from django.contrib import messages from django.core.urlresolvers import reverse from django.conf import settings from django.http import Http404, HttpResponseRedirect from wafer.registration.sso import SSOError, debian_sso, github_sso def redirect_profile(request): ''' The default destination from logging in, redirect to the actual profile URL ''' if request.user.is_authenticated(): return HttpResponseRedirect(reverse('wafer_user_profile', args=(request.user.username,))) else: return redirect_to_login(next=reverse(redirect_profile)) def github_login(request): if 'github' not in settings.WAFER_SSO: raise Http404() if 'code' not in request.GET: return HttpResponseRedirect( 'https://github.com/login/oauth/authorize?' + urllib.urlencode({ 'client_id': settings.WAFER_GITHUB_CLIENT_ID, 'redirect_uri': request.build_absolute_uri( reverse(github_login)), 'scope': 'user:email', 'state': request.META['CSRF_COOKIE'], })) try: if request.GET['state'] != request.META['CSRF_COOKIE']: raise SSOError('Incorrect state') user = github_sso(request.GET['code']) except SSOError as e: messages.error(request, u'%s' % e) return HttpResponseRedirect(reverse('auth_login')) login(request, user) return redirect_profile(request) def debian_login(request): if 'debian' not in settings.WAFER_SSO: raise Http404() try: user = debian_sso(request.META) except SSOError as e: messages.error(request, u'%s' % e) return HttpResponseRedirect(reverse('auth_login')) login(request, user) return redirect_profile(request) PK}Hwafer/registration/forms.pyfrom django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit class RegistrationFormHelper(FormHelper): form_action = reverse('registration_register') def __init__(self, *args, **kwargs): super(RegistrationFormHelper, self).__init__(*args, **kwargs) self.add_input(Submit('submit', _('Sign up'))) class LoginFormHelper(FormHelper): form_action = reverse('django.contrib.auth.views.login') def __init__(self, *args, **kwargs): super(LoginFormHelper, self).__init__(*args, **kwargs) # TODO: next field self.add_input(Submit('submit', _('Log in'))) PKv|Gwafer/registration/apps.py# Needed due to django 1.7 changed app name restrictions from django.apps import AppConfig class RegistrationConfig(AppConfig): label = 'wafer.registration' name = 'wafer.registration' PKOIG BBwafer/registration/__init__.pydefault_app_config = 'wafer.registration.apps.RegistrationConfig' PK(Hpwafer/registration/sso.py# coding: utf-8 import logging from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db import IntegrityError import requests from wafer.kv.models import KeyValue MAX_APPEND = 20 log = logging.getLogger(__name__) class SSOError(Exception): pass def sso(user, desired_username, name, email, profile_fields=None): """ Create a user, if the provided `user` is None, from the parameters. Then log the user in, and return it. """ if not user: user = _create_desired_user(desired_username) _configure_user(user, name, email, profile_fields) if not user.is_active: raise SSOError('Account disabled') # login() expects the logging in backend to be set on the user. # We are bypassing login, so fake it. user.backend = settings.AUTHENTICATION_BACKENDS[0] return user def _create_desired_user(desired_username): for append in xrange(MAX_APPEND): username = desired_username if append: username += str(append) try: return get_user_model().objects.create(username=username) except IntegrityError: continue log.warning('Ran out of possible usernames for %s', desired_username) raise SSOError('Ran out of possible usernames for %s' % desired_username) def _configure_user(user, name, email, profile_fields): if name: user.first_name, user.last_name = name for attr in ('first_name', 'last_name'): max_length = get_user_model()._meta.get_field(attr).max_length if len(getattr(user, attr)) > max_length: setattr(user, attr, getattr(user, attr)[:max_length - 1] + u'…') user.email = email user.save() profile = user.userprofile if profile_fields: for k, v in profile_fields.items(): setattr(profile, k, v) profile.save() def github_sso(code): r = requests.post('https://github.com/login/oauth/access_token', data={ 'client_id': settings.WAFER_GITHUB_CLIENT_ID, 'client_secret': settings.WAFER_GITHUB_CLIENT_SECRET, 'code': code, }) if r.status_code != 200: log.warning('Response %s from api.github.com', r.status_code) raise SSOError('Invalid code') token = r.content r = requests.get('https://api.github.com/user?%s' % token) if r.status_code != 200: log.warning('Response %s from api.github.com', r.status_code) raise SSOError('Failed response from GitHub') gh = r.json() try: login = gh['login'] name = gh['name'].partition(' ')[::2] except KeyError as e: log.warning('Error creating account from github information: %s', e) raise SSOError('GitHub profile missing required content') email = gh.get('email', None) if not email: # No public e-mail address r = requests.get('https://api.github.com/user/emails?%s' % token) if r.status_code != 200: log.warning('Response %s from api.github.com', r.status_code) raise SSOError('Failed response from GitHub') try: email = r.json()[0]['email'] except (KeyError, IndexError) as e: log.warning('Error extracting github email address: %s', e) raise SSOError('Failed to obtain email address from GitHub') profile_fields = { 'github_username': login, } if 'blog' in gh: profile_fields['blog'] = gh['blog'] try: user = get_user_model().objects.get(userprofile__github_username=login) except MultipleObjectsReturned: log.warning('Multiple accounts have GitHub username %s', login) raise SSOError('Multiple accounts have GitHub username %s' % login) except ObjectDoesNotExist: user = None user = sso(user=user, desired_username=login, name=name, email=email, profile_fields=profile_fields) return user def debian_sso(meta): authentication_status = meta.get('SSL_CLIENT_VERIFY', None) if authentication_status != "SUCCESS": raise SSOError('Requires authentication via Client Certificate. ' 'Obtain one from https://sso.debian.org/spkac/') email = meta['SSL_CLIENT_S_DN_CN'] group = Group.objects.get_by_natural_key('Registration') user = None for kv in KeyValue.objects.filter( group=group, key='debian_sso_email', value=email, userprofile__isnull=False): if kv.userprofile_set.count() > 1: message = 'Multiple accounts have Debian SSOed with address %s' log.warning(message, email) raise SSOError(message % email) user = kv.userprofile_set.first().user break username = email.split('@', 1)[0] name = ('Unknown User', username) if not user: r = requests.get('https://nm.debian.org/api/people', params={'uid': username}, headers={'Api-Key': settings.WAFER_DEBIAN_NM_API_KEY}) if r.status_code != 200: log.warning('Response %s from nm.debian.org', r.status_code) raise SSOError('Failed to query nm.debian.org') if 'r' not in r.json(): log.warning('Error parsing nm.debian.org response: %r', r.json()) raise SSOError('Failed to parse nm.debian.org respnose') # The API performs substring queries, so we need to find the correct # entry in the response. for person in r.json()['r']: if person['uid'] == username: first_name = person['cn'] if person['mn']: first_name += u' ' + person['mn'] last_name = person['sn'] name = (first_name, last_name) break user = sso(user=user, desired_username=username, name=name, email=email) user.userprofile.kv.get_or_create(group=group, key='debian_sso_email', defaults={'value': email}) return user PKOIG$ Bwafer/registration/templates/registration/activation_complete.html{% extends 'wafer/base.html' %} {% load i18n %} {% block content %}

{% trans 'Activation complete' %}

{% url 'auth_login' as login_url %} {% blocktrans %} You are registered! Please log in. {% endblocktrans %}

{% endblock %} PKOIGҽx}5wafer/registration/templates/registration/logout.html{% extends 'wafer/base.html' %} {% load i18n %} {% block content %}

{% trans 'Logged out' %}

{% blocktrans %} You've been logged out. {% endblocktrans %}

{% endblock %} PK}He9@wafer/registration/templates/registration/registration_form.html{% extends 'wafer/base.html' %} {% load i18n %} {% load crispy_forms_tags %} {% load wafer_crispy %} {% block content %}

{% trans 'Sign up' %}

{% wafer_form_helper 'wafer.registration.forms.RegistrationFormHelper' as form_helper %} {% crispy form form_helper %} {% endblock %} PKOIGx;\\Dwafer/registration/templates/registration/registration_complete.html{% extends 'wafer/base.html' %} {% load i18n %} {% block content %}

{% trans 'Registration almost complete' %}

{% blocktrans %} Thank you for registering for {{ WAFER_CONFERENCE_NAME }}. We have sent you an e-mail. Please follow the instructions in it to complete your registration. {% endblocktrans %}

{% endblock %} PKOIG@/>wafer/registration/templates/registration/activation_email.txt{% load i18n %} {% spaceless %} {% url 'registration_activate' activation_key as activation_url %} {% url 'index' as index_url %} {% endspaceless %}{% blocktrans with WAFER_CONFERENCE_NAME=site.name domain=site.domain %}Hi, TL;DR Click https://{{ domain }}{{ activation_url }} Thank you for signing up for {{ WAFER_CONFERENCE_NAME }}. To verify your e-mail address, please follow the link above, and complete your registration process. You have {{ expiration_days }} days left to do so. We hope to see you soon. Thanks, The {{ WAFER_CONFERENCE_NAME }} website. -- {{ WAFER_CONFERENCE_NAME }} http://{{ domain }}{{ index_url }} {% endblocktrans %} PKv|G'O֦))4wafer/registration/templates/registration/login.html{% extends 'wafer/base.html' %} {% load i18n %} {% load crispy_forms_tags %} {% load wafer_crispy %} {% block content %}

{% trans 'Log in' %}

{% wafer_form_helper 'wafer.registration.forms.LoginFormHelper' as form_helper %} {% crispy form form_helper %}

{% trans 'Sign up' %}

{% url 'registration_register' as signup_url %} {% blocktrans %} Not registered? Please sign up. {% endblocktrans %} {% if WAFER_SSO %}

{% trans 'Shared/Social Log in and Sign up' %}

    {% if 'github' in WAFER_SSO %} {% url 'wafer.registration.views.github_login' as github_sso_url %}
  • GitHub
  • {% endif %} {% if 'debian' in WAFER_SSO %} {% url 'wafer.registration.views.debian_login' as debian_sso_url %}
  • Debian SSO
  • {% endif %}
{% endif %}
{% endblock %} PKOIGY®Fwafer/registration/templates/registration/activation_email_subject.txt{% load i18n %} {% blocktrans with WAFER_CONFERENCE_NAME=site.name %} Activate your {{ WAFER_CONFERENCE_NAME }} account [{{ expiration_days }} days left] {% endblocktrans %} PKOIG8ׁ7wafer/registration/templates/registration/activate.html{% extends 'wafer/base.html' %} {% load i18n %} {% block content %}

{% trans 'Oops, Registration failed' %}

{% url 'registration_register' as register_url %} {% url 'wafer_page' 'contact' as contact_url %} {% blocktrans %} We seem to have had some difficulty with our registration. Please try again or contact our admins. {% endblocktrans %}

{% endblock %} PKv|G' @wafer/registration/templates/registration/registration_base.html{% extends 'wafer/base.html' %} PKOIG+wafer/registration/templatetags/__init__.pyPKOIGx/wafer/registration/templatetags/wafer_crispy.pyfrom django import template import sys register = template.Library() @register.assignment_tag def wafer_form_helper(helper_name): ''' Find the specified Crispy FormHelper and instantiate it. Handy when you are crispyifying other apps' forms. ''' module, class_name = helper_name.rsplit('.', 1) if module not in sys.modules: __import__(module) mod = sys.modules[module] class_ = getattr(mod, class_name) return class_() PKOIG)wafer/registration/migrations/__init__.pyPK}Hwafer/compare/__init__.pyPKyItMMwafer/compare/admin.py# hack'ish support for comparing a django reversion # history object with the current state from diff_match_patch import diff_match_patch import datetime from reversion.admin import VersionAdmin from reversion.models import Version from django.conf.urls import url from django.shortcuts import get_object_or_404, render from django.contrib.admin.utils import unquote, quote from django.core.urlresolvers import reverse from django.utils.translation import ugettext as _ from django.utils.encoding import force_text from django.contrib.admin import SimpleListFilter from django.contrib.contenttypes.models import ContentType from markitup.fields import Markup class DateModifiedFilter(SimpleListFilter): title = _('Last Modified') parameter_name = 'moddate' def lookups(self, request, model_admin): return ( ('today', _('Today')), ('yesterday', _('Since yesterday')), ('7 days', _('In the last 7 days')), ('30 days', _('In the last 30 days')), ) def queryset(self, request, queryset): date = None if self.value() == 'today': date = datetime.date.today() elif self.value() == 'yesterday': date = datetime.date.today() - datetime.timedelta(days=1) elif self.value() == '7 days': date = datetime.date.today() - datetime.timedelta(days=7) elif self.value() == '30 days': date = datetime.date.today() - datetime.timedelta(days=30) if not date: return queryset content_type = ContentType.objects.get_for_model(queryset.model) revisions = Version.objects.filter(content_type=content_type, revision__date_created__gte=date) return queryset.filter(pk__in=[x.object_id for x in revisions]) def get_date(revision): return revision.revision.date_created.strftime("%Y-%m-%d %H:%m:%S") def get_author(revision): if revision.revision.user: return revision.revision.user.username return 'Unknown' def make_diff(current, revision): """Create the difference between the current revision and a previous version""" the_diff = [] dmp = diff_match_patch() for field in (set(current.field_dict.keys()) | set(revision.field_dict.keys())): # These exclusions really should be configurable if field == 'id' or field.endswith('_rendered'): continue # KeyError's may happen if the database structure changes # between the creation of revisions. This isn't ideal, # but should not be a fatal error. # Log this? missing_field = False try: cur_val = current.field_dict[field] or "" except KeyError: cur_val = "No such field in latest version\n" missing_field = True try: old_val = revision.field_dict[field] or "" except KeyError: old_val = "No such field in old version\n" missing_field = True if missing_field: # Ensure that the complete texts are marked as changed # so new entries containing any of the marker words # don't show up as differences diffs = [(dmp.DIFF_DELETE, old_val), (dmp.DIFF_INSERT, cur_val)] patch = dmp.diff_prettyHtml(diffs) elif isinstance(cur_val, Markup): # we roll our own diff here, so we can compare of the raw # markdown, rather than the rendered result. if cur_val.raw == old_val.raw: continue diffs = dmp.diff_main(old_val.raw, cur_val.raw) patch = dmp.diff_prettyHtml(diffs) elif cur_val == old_val: continue else: # Compare the actual field values diffs = dmp.diff_main(force_text(old_val), force_text(cur_val)) patch = dmp.diff_prettyHtml(diffs) the_diff.append((field, patch)) the_diff.sort() return the_diff class CompareVersionAdmin(VersionAdmin): compare_template = "admin/wafer.compare/compare.html" compare_list_template = "admin/wafer.compare/compare_list.html" # Add a compare button next to the History button. change_form_template = "admin/wafer.compare/change_form.html" def get_urls(self): urls = super(CompareVersionAdmin, self).get_urls() opts = self.model._meta compare_urls = [ url("^([^/]+)/([^/]+)/compare/$", self.admin_site.admin_view(self.compare_view), name='%s_%s_compare' % (opts.app_label, opts.model_name)), url("^([^/]+)/comparelist/$", self.admin_site.admin_view(self.comparelist_view), name='%s_%s_comparelist' % (opts.app_label, opts.model_name)), ] return compare_urls + urls def compare_view(self, request, object_id, version_id, extra_context=None): """Actually compare two versions.""" opts = self.model._meta object_id = unquote(object_id) # get_for_object's ordering means this is always the latest revision. # The reversion we want to compare to current = Version.objects.get_for_object_reference(self.model, object_id)[0] revision = Version.objects.get_for_object_reference(self.model, object_id).filter(id=version_id)[0] the_diff = make_diff(current, revision) context = { "title": _("Comparing current %(model)s with revision created %(date)s") % { 'model': current, 'date' : get_date(revision), }, "opts": opts, "compare_list_url": reverse("%s:%s_%s_comparelist" % (self.admin_site.name, opts.app_label, opts.model_name), args=(quote(object_id),)), "diff_list": the_diff, } extra_context = extra_context or {} context.update(extra_context) return render(request, self.compare_template or self._get_template_list("compare.html"), context) def comparelist_view(self, request, object_id, extra_context=None): """Allow selecting versions to compare.""" opts = self.model._meta object_id = unquote(object_id) current = get_object_or_404(self.model, pk=object_id) # As done by reversion's history_view action_list = [ { "revision": version.revision, "url": reverse("%s:%s_%s_compare" % (self.admin_site.name, opts.app_label, opts.model_name), args=(quote(version.object_id), version.id)), } for version in self._reversion_order_version_queryset(Version.objects.get_for_object_reference( self.model, object_id).select_related("revision__user"))] context = {"action_list": action_list, "opts": opts, "object_id": quote(object_id), "original": current, } extra_context = extra_context or {} context.update(extra_context) return render(request, self.compare_list_template or self._get_template_list("compare_list.html"), context) PK}H8,,8wafer/compare/templates/admin/wafer.compare/compare.html{% extends "admin/base_site.html" %} {% load i18n l10n admin_urls %} {% block breadcrumbs %} {% endblock %} {% block content %} {% for field, diff in diff_list %}

{{ field }}

{{ diff | safe }}
{% empty %}

{% trans 'No differences found' %}

{% endfor %} {% endblock %} PK}H{9tt<wafer/compare/templates/admin/wafer.compare/change_form.html{% extends "admin/change_form.html" %} {% load i18n admin_urls %} {% block object-tools-items %}
  • {% trans "History" %}
  • {% trans "Compare Versions" %}
  • {% if has_absolute_url %}
  • {% trans "View on site" %}
  • {% endif %} {% endblock %} PK}H,?[p =wafer/compare/templates/admin/wafer.compare/compare_list.html{% extends "admin/base_site.html" %} {% load i18n l10n admin_urls %} {% block breadcrumbs %} {% endblock %} {% block content %}

    {% blocktrans %}Choose a date from the list below to compare the current version to a previous version of this object.{% endblocktrans %}

    {% if action_list %} {% for action in action_list %} {% endfor %}
    {% trans 'Date/time' %} {% trans 'User' %} {% trans 'Comment' %}
    {{action.revision.date_created}} {% if action.revision.user %} {{action.revision.user.get_username}} {% if action.revision.user.get_full_name %} ({{action.revision.user.get_full_name}}){% endif %} {% else %} — {% endif %} {{action.revision.comment|linebreaksbr|default:""}}
    {% else %}

    {% trans "This object doesn't have a change history. It probably wasn't added via this admin site." %}

    {% endif %}
    {% endblock %} PK}H|}}wafer/sponsors/urls.pyfrom django.conf.urls import patterns, url, include from rest_framework import routers from wafer.sponsors.views import ( ShowSponsors, SponsorView, ShowPackages, SponsorViewSet, PackageViewSet) router = routers.DefaultRouter() router.register(r'sponsors', SponsorViewSet) router.register(r'packages', PackageViewSet) urlpatterns = patterns( '', url(r'^$', ShowSponsors.as_view(), name='wafer_sponsors'), url(r'^(?P\d+)/$', SponsorView.as_view(), name='wafer_sponsor'), url(r'^packages/$', ShowPackages.as_view(), name='wafer_sponsorship_packages'), url(r'^api/', include(router.urls)), ) PKyI wafer/sponsors/views.pyfrom django.views.generic.list import ListView from django.views.generic import DetailView from rest_framework import viewsets from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly from wafer.sponsors.models import Sponsor, SponsorshipPackage from wafer.sponsors.serializers import SponsorSerializer, PackageSerializer class ShowSponsors(ListView): template_name = 'wafer.sponsors/sponsors.html' model = Sponsor def get_queryset(self): return Sponsor.objects.all().order_by('packages', 'order', 'id') class SponsorView(DetailView): template_name = 'wafer.sponsors/sponsor.html' model = Sponsor class ShowPackages(ListView): template_name = 'wafer.sponsors/packages.html' model = SponsorshipPackage class SponsorViewSet(viewsets.ModelViewSet): """API endpoint for users.""" queryset = Sponsor.objects.all() serializer_class = SponsorSerializer permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) class PackageViewSet(viewsets.ModelViewSet): """API endpoint for users.""" queryset = SponsorshipPackage.objects.all() serializer_class = PackageSerializer permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) PKyIܞV wafer/sponsors/models.pyfrom django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.core.validators import MinValueValidator from django.db import models from django.utils.encoding import python_2_unicode_compatible from markitup.fields import MarkupField @python_2_unicode_compatible class File(models.Model): """A file for use in sponsor and sponshorship package descriptions.""" name = models.CharField(max_length=255) description = models.TextField() item = models.FileField(upload_to='sponsors_files') def __str__(self): return u'%s' % (self.name,) @python_2_unicode_compatible class SponsorshipPackage(models.Model): """A description of a sponsorship package.""" order = models.IntegerField(default=1) name = models.CharField(max_length=255) number_available = models.IntegerField( null=True, validators=[MinValueValidator(0)]) currency = models.CharField( max_length=16, default='$', help_text=_("Currency symbol for the sponsorship amount.")) price = models.DecimalField( max_digits=12, decimal_places=2, help_text=_("Amount to be sponsored.")) short_description = models.TextField( help_text=_("One sentence overview of the package.")) description = MarkupField( help_text=_("Describe what the package gives the sponsor.")) files = models.ManyToManyField( File, related_name="packages", blank=True, help_text=_("Images and other files for use in" " the description markdown field.")) class Meta: ordering = ['order', '-price', 'name'] def __str__(self): return u'%s (amount: %.0f)' % (self.name, self.price) def number_claimed(self): return self.sponsors.count() @python_2_unicode_compatible class Sponsor(models.Model): """A conference sponsor.""" order = models.IntegerField(default=1) name = models.CharField(max_length=255) packages = models.ManyToManyField(SponsorshipPackage, related_name="sponsors") description = MarkupField( help_text=_("Write some nice things about the sponsor.")) files = models.ManyToManyField( File, related_name="sponsors", blank=True, help_text=_("Images and other files for use in" " the description markdown field.")) url = models.URLField(default="", blank=True, help_text=_("Url to link back to the sponsor if required")) class Meta: ordering = ['order', 'name', 'id'] def __str__(self): return u'%s' % (self.name,) def get_absolute_url(self): return reverse('wafer_sponsor', args=(self.pk,)) @property def logo(self): return self.files.get(name='logo').item PKOIG/}  wafer/sponsors/renderers.pyfrom django_medusa.renderers import StaticSiteRenderer from wafer.sponsors.models import Sponsor from django.core.urlresolvers import reverse class SponsorRenderer(StaticSiteRenderer): def get_paths(self): paths = ["/sponsors/", ] items = Sponsor.objects.all() for item in items: paths.append(item.get_absolute_url()) paths.append(reverse('wafer_sponsors')) paths.append(reverse('wafer_sponsorship_packages')) return paths renderers = [SponsorRenderer, ] PKOIGwafer/sponsors/__init__.pyPKKHzUwafer/sponsors/admin.pyfrom django.contrib import admin from wafer.sponsors.models import File, SponsorshipPackage, Sponsor from reversion.admin import VersionAdmin class SponsorAdmin(VersionAdmin, admin.ModelAdmin): pass class SponsorshipPackageAdmin(VersionAdmin, admin.ModelAdmin): pass admin.site.register(SponsorshipPackage, SponsorshipPackageAdmin) admin.site.register(Sponsor, SponsorAdmin) admin.site.register(File) PK}Hc11wafer/sponsors/serializers.pyfrom rest_framework import serializers from reversion import revisions from wafer.sponsors.models import Sponsor, SponsorshipPackage class SponsorSerializer(serializers.ModelSerializer): class Meta: model = Sponsor exclude = ('_description_rendered', ) @revisions.create_revision() def create(self, validated_data): revisions.set_comment("Created via REST api") return super(SponsorSerializer, self).create(validated_data) @revisions.create_revision() def update(self, sponsor, validated_data): revisions.set_comment("Changed via REST api") sponsor.name = validated_data['name'] sponsor.description = validated_data['description'] sponsor.save() return sponsor class PackageSerializer(serializers.ModelSerializer): class Meta: model = SponsorshipPackage exclude = ('_description_rendered', ) @revisions.create_revision() def create(self, validated_data): revisions.set_comment("Created via REST api") return super(PackageSerializer, self).create(validated_data) @revisions.create_revision() def update(self, package, validated_data): revisions.set_comment("Changed via REST api") package.name = validated_data['name'] package.number_available = validated_data['number_available'] package.description = validated_data['description'] package.short_description = validated_data['short_description'] package.price = validated_data['price'] package.save() return package PKyIm)||5wafer/sponsors/templates/wafer.sponsors/sponsors.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {% trans 'Sponsors' %}

    {% for sponsor in sponsor_list %}

    {% if sponsor.packages.first.name.lower == 'platinum' %} ★ {% endif %} {% if sponsor.url %} {{ sponsor.name }} {% else %} {{ sponsor.name }} {% endif %} {{ sponsor.packages.first.name }}

    {{ sponsor.description.rendered|safe }}

    {% empty %}

    No sponsors yet.

    {% endfor %}
    {% endblock %} PKyITWW4wafer/sponsors/templates/wafer.sponsors/sponsor.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {% if object.url %} {{ object.name }} {% else %} {{ object.name }} {% endif %}

    {% endblock %} PK}H/Ǐ;wafer/sponsors/templates/wafer.sponsors/sponsors_block.html
    {% for package in packages %} {% if package.sponsors.exists %}
    {{ package.name }}
      {% for sponsor in package.sponsors.all %}
    • {% if sponsor.logo %} {{ sponsor.name }} {% else %} {% endif %}
    • {% endfor %}
    {% endif %} {% endfor %}
    PKyIjk0[[5wafer/sponsors/templates/wafer.sponsors/packages.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {% trans 'Sponsorship Packages' %}

    {% for package in sponsorshippackage_list %}

    {{ package.name }} ({{ package.currency }} {{ package.price|floatformat:0 }})

    {% if package.number_available %}

    {% trans 'Total number available:' %} {{ package.number_available }}

    {% endif %}

    {% trans 'Packages claimed:' %} {{ package.number_claimed }}

    {{ package.short_description }}

    {{ package.description.rendered|safe }}
    {% empty %}

    No sponsorship packages available yet.

    {% endfor %}
    {% endblock %} PK}H'wafer/sponsors/templatetags/__init__.pyPK}H{4mm'wafer/sponsors/templatetags/sponsors.pyfrom django import template from wafer.sponsors.models import Sponsor, SponsorshipPackage register = template.Library() @register.inclusion_tag('wafer.sponsors/sponsors_block.html') def sponsors(): return { 'sponsors': Sponsor.objects.all().order_by('packages'), 'packages': SponsorshipPackage.objects.all().prefetch_related('files'), } PKOIGV%P P )wafer/sponsors/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import django.core.validators import markitup.fields class Migration(migrations.Migration): dependencies = [ ] operations = [ migrations.CreateModel( name='File', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), ('description', models.TextField()), ('item', models.FileField(upload_to=b'sponsors_files')), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Sponsor', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), ('description', markitup.fields.MarkupField(help_text='Write some nice things about the sponsor.', no_rendered_field=True)), ('_description_rendered', models.TextField(editable=False, blank=True)), ('files', models.ManyToManyField(help_text='Images and other files for use in the description markdown field.', related_name='sponsors', null=True, to='sponsors.File', blank=True)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='SponsorshipPackage', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('order', models.IntegerField(default=1)), ('name', models.CharField(max_length=255)), ('number_available', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(0)])), ('currency', models.CharField(default=b'$', help_text='Currency symbol for the sponsorship amount.', max_length=16)), ('price', models.DecimalField(help_text='Amount to be sponsored.', max_digits=12, decimal_places=2)), ('short_description', models.TextField(help_text='One sentence overview of the package.')), ('description', markitup.fields.MarkupField(help_text='Describe what the package gives the sponsor.', no_rendered_field=True)), ('_description_rendered', models.TextField(editable=False, blank=True)), ('files', models.ManyToManyField(help_text='Images and other files for use in the description markdown field.', related_name='packages', null=True, to='sponsors.File', blank=True)), ], options={ 'ordering': ['order', '-price', 'name'], }, bases=(models.Model,), ), migrations.AddField( model_name='sponsor', name='packages', field=models.ManyToManyField(related_name='sponsors', to='sponsors.SponsorshipPackage'), preserve_default=True, ), ] PKKH<N**0wafer/sponsors/migrations/0002_non-null_files.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('sponsors', '0001_initial'), ] operations = [ migrations.AlterField( model_name='sponsor', name='files', field=models.ManyToManyField(help_text='Images and other files for use in the description markdown field.', related_name='sponsors', to='sponsors.File', blank=True), ), migrations.AlterField( model_name='sponsorshippackage', name='files', field=models.ManyToManyField(help_text='Images and other files for use in the description markdown field.', related_name='packages', to='sponsors.File', blank=True), ), ] PKOIG%wafer/sponsors/migrations/__init__.pyPKyI4wafer/sponsors/migrations/0004_auto_20160813_1328.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('sponsors', '0003_add_ordering_option'), ] operations = [ migrations.AlterModelOptions( name='sponsor', options={'ordering': ['order', 'name', 'id']}, ), migrations.AddField( model_name='sponsor', name='order', field=models.IntegerField(default=1), ), migrations.AddField( model_name='sponsor', name='url', field=models.URLField(default=b'', help_text='Url to link back to the sponsor if required', blank=True), ), ] PK(H5wafer/sponsors/migrations/0003_add_ordering_option.py# -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-04-07 10:51 from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('sponsors', '0002_non-null_files'), ] operations = [ migrations.AlterModelOptions( name='sponsor', options={'ordering': ['id']}, ), ] PKHqF wafer/static/js/edit_schedule.js(function () { 'use strict'; jQuery.event.props.push('dataTransfer'); var handleDragStart = function (e) { e.target.style.opacity = '0.4'; // this / e.target is the source node. e.target.classList.add('label-danger'); //noinspection JSUnresolvedVariable e.dataTransfer.effectAllowed = 'move'; //noinspection JSUnresolvedVariable e.dataTransfer.setData('text/plain', this.id); }; var handleDragEnd = function (e) { e.target.style.opacity = '1'; // this / e.target is the source node. e.target.classList.remove('label-danger'); }; function handleDragOver(e) { if (e.preventDefault) { e.preventDefault(); // Necessary. Allows us to drop. } //noinspection JSUnresolvedVariable e.dataTransfer.dropEffect = 'move'; return false; } function handleDragEnter(e) { e.target.classList.add('over'); } function handleDragLeave(e) { e.target.classList.remove('over'); } function handleItemUpdate(data) { console.log(data); var scheduleItemId = data.id; var venue = data.venue; var slots = data.slots; var talkId = data.talk; var pageId = data.page; var scheduleItemType; if (talkId) { scheduleItemType = 'talk'; } else if (pageId) { scheduleItemType = 'page'; } var newItem = document.querySelectorAll('[id=scheduleItemnull]')[0]; newItem.id = 'scheduleItem' + scheduleItemId; } function handleItemDelete() { console.log(this); } function handleDrop(e) { // this / e.target is current target element. e.target.classList.remove('over'); if (e.stopPropagation) { e.stopPropagation(); // stops the browser from redirecting. } var slot = e.target.getAttribute('data-slot'); var venue = e.target.getAttribute('data-venue'); //noinspection JSUnresolvedVariable var data = document.getElementById( event.dataTransfer.getData('text/plain')); var scheduleItemId = data.getAttribute('data-scheduleitem-id'); var scheduleItemType = data.getAttribute('data-type'); event.target.innerHTML = data.getAttribute('title'); event.target.setAttribute('data-schedule-item-id', scheduleItemId); event.target.setAttribute('data-type', scheduleItemType); event.target.id = 'scheduleItem' + scheduleItemId; event.preventDefault(); var talkId = ''; var pageId = ''; if (scheduleItemType === 'talk') { talkId = data.getAttribute('data-talk-id'); } else if (scheduleItemType === 'page') { pageId = data.getAttribute('data-page-id'); } event.target.classList.remove('success'); event.target.classList.remove('info'); var typeClass = scheduleItemType === 'talk' ? 'success' : 'info'; event.target.classList.add(typeClass); var ajaxData = { talk: talkId, page: pageId }; if (scheduleItemId) { $.ajax({ method: 'PATCH', url: '/schedule/api/scheduleitems/' + scheduleItemId + '/', data: JSON.stringify(ajaxData), success: handleItemUpdate }); } else { ajaxData.venue = venue; ajaxData.slots = [slot]; console.log(ajaxData); $.post( '/schedule/api/scheduleitems/', JSON.stringify(ajaxData), handleItemUpdate); } return false; } function handleClickDelete(mouseEvent) { var closeButton = mouseEvent.path[1]; var scheduleItemCell = mouseEvent.path[2]; var scheduleItemId = closeButton.getAttribute('data-id'); scheduleItemCell.removeAttribute('id'); scheduleItemCell.classList.remove('draggable'); scheduleItemCell.classList.remove('info'); scheduleItemCell.classList.remove('success'); scheduleItemCell.removeAttribute('data-scheduleitem-id'); scheduleItemCell.removeAttribute('data-talk-id'); scheduleItemCell.removeAttribute('data-page-id'); scheduleItemCell.removeAttribute('data-type'); closeButton.removeAttribute('data-id'); closeButton.classList.add('hide'); scheduleItemCell.innerHTML = ''; $.ajax( { type: 'DELETE', url: '/schedule/api/scheduleitems/' + scheduleItemId + '/', success: handleItemDelete } ); } function getCookie(name) { var cookieValue = null; if (document.cookie && document.cookie !== '') { var cookies = document.cookie.split(';'); for (var i = 0; i < cookies.length; i++) { var cookie = jQuery.trim(cookies[i]); // Does this cookie string begin with the name we want? if (cookie.substring(0, name.length + 1) === (name + '=')) { cookieValue = decodeURIComponent( cookie.substring(name.length + 1)); break; } } } return cookieValue; } var csrftoken = getCookie('csrftoken'); function csrfSafeMethod(method) { // these HTTP methods do not require CSRF protection return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); } $.ajaxSetup({ beforeSend: function (xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader('X-CSRFToken', csrftoken); xhr.setRequestHeader('Content-Type', 'application/json'); } } }); var draggableItems = document.querySelectorAll('.draggable'); [].forEach.call(draggableItems, function (draggableItem) { draggableItem.addEventListener('dragstart', handleDragStart, false); draggableItem.addEventListener('dragend', handleDragEnd, false); draggableItem.addEventListener('dragover', handleDragOver, false); }); var droppableItems = document.querySelectorAll('.droppable'); [].forEach.call(droppableItems, function (droppableItem) { droppableItem.addEventListener('dragover', handleDragOver, false); droppableItem.addEventListener('dragenter', handleDragEnter, false); droppableItem.addEventListener('dragleave', handleDragLeave, false); droppableItem.addEventListener('drop', handleDrop, false); }); var deletableItems = document.querySelectorAll('[id^=delete]'); [].forEach.call(deletableItems, function (deletableItem) { deletableItem.addEventListener('click', handleClickDelete, false); }); })(); PKOIGv#wafer/static/js/scheduledatetime.js// Basic idea from Bojan Mihelac - // http://source.mihelac.org/2010/02/19/django-time-widget-custom-time-shortcuts/ // Django imports jQuery into the admin site using noConflict(True) // We wrap this in an anonymous function to stick to jQuery's $ idiom // and ensure we're using the admin version of jQuery, rather than // anything else pulled in (function($) { var Schedule = { init: function() { time_format = get_format('TIME_INPUT_FORMATS')[0]; $(".timelist").each(function(num) { // Clear existing list $( this ).empty(); // Fill list with time values for (i=8; i < 20; i++) { var time = new Date(1970,1,1,i,0,0); quickElement("a", quickElement("li", this, ""), time.strftime(time_format), "href", "javascript:DateTimeShortcuts.handleClockQuicklink(" + num + ", " + i + ");"); } }); } } // We need to be called after django's DateTimeShortcuts.init, which // is triggered by a load event $( window ).bind('load', Schedule.init); }(django.jQuery)); PKyI"&±IIwafer/static/css/wafer.css#profile-avatar { position: relative; } #profile-avatar img { position: relative; margin-bottom: 10px; } #profile-avatar .popover { width: 20em; } #profile-avatar .popover-contents { display: none; } .profile-links li{ list-style: none; display: inline-block; margin-left: 5px; } [draggable] { -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; user-select: none; /* Required to make elements draggable in old WebKit */ -khtml-user-drag: element; -webkit-user-drag: element; } .draggable { cursor: move; padding: 5px; } .droppable.over{ border: 5px dashed #000; } .bs-callout { padding: 5px; margin: 20px 0; border: 1px solid #eee; border-left-width: 10px; border-radius: 6px; } .bs-callout h4 { margin-top: 0; margin-bottom: 5px; } .bs-callout p:last-child { margin-bottom: 0; } .bs-callout code { border-radius: 3px; } .bs-callout+.bs-callout { margin-top: -5px; } .bs-callout-default { border-left-color: #777; } .bs-callout-default h4 { color: #777; } .bs-callout-primary { border-left-color: #428bca; } .bs-callout-primary h4 { color: #428bca; } .bs-callout-success { border-left-color: #5cb85c; } .bs-callout-success h4 { color: #5cb85c; } .bs-callout-danger { border-left-color: #d9534f; } .bs-callout-danger h4 { color: #d9534f; } .bs-callout-warning { border-left-color: #f0ad4e; } .bs-callout-warning h4 { color: #f0ad4e; } .bs-callout-info { border-left-color: #5bc0de; } .bs-callout-info h4 { color: #5bc0de; } .bs-callout-platinum { border-left-color: #E5E4E2; } .bs-callout-gold { border-left-color: #DAA520; } .bs-callout-silver { border-left-color: #C0C0C0; } .bs-callout-bronze { border-left-color: #CD7F32; } .bs-callout-patron { border-left-color: #A4C639; } PKOIGV(F11)wafer/static/img/glyphicons-halflings.pngPNG  IHDRtEXtSoftwareAdobe ImageReadyqe<1IDATx}ml\EW^ɺD$|nw';vю8m0kQSnSV;1KGsԩ>UoTU1cƖYuּca&#C,pؚ>kں ULW -sn3Vq~NocI~L{- H8%_M£wB6EW,ĢpY2+(Y@&A/3kXhߍ-aA<>P'\J;(}#Qz:4%m?nfntK*l9J+DIYu1YZ^(]YYEf@ОlXz]Ut u &5-PW}@t|#LY=s܂,w#+R+?Ƌax X0"ea)tG*ԡwVwV^rf%xB(qּ4>WG#lWU<ЁXJVѶlR$kDVrI7:X%X1NEzw;y9z9O%~~uɗ*=Ixcy}Y(ou ±N$^j e\iX񝜬];Y-rѲ&>!zlYaVHVN԰9=]=mRMdOUC JUiT}rWW'ڹu)ʢF"YU#P׾&ܑЅROwyzm$Os? +^FTIEq%&~ >M}]ԖwA? [Nteexn(措BdMTpʥnqqS?bWXmW6x*{V_!VjΧsVL^j XkQjU6sk̩n~[qǸ-` O:G7l"ksRe2vQ=QƼJUX`gQy~ ďKȰE]#P:td\T/u;س:Jc-%'e q ?j"/yh48Zi1|JUu>_N;hxwNU JQU7\j̮bT:B?6oJ1Ί%I UY-Ii4{=rǤ7@)HKJ+f4X8Cd?'j1 N< 39EWo VTGzg# %D0#ܠ3[tiآ( U,]125|Ṋfw7w u+Š]Db]K xbW ՛7|ВX㕛{UcGXk¬|(h)IUa)lp 3luPU]D)/7~4Wt5J}V X0z VM;>Gԙ^|gF:jaZ^)74C#jwr,еSlGu;1vm><)}ZQՖ&mZ:1UMB~ a:/᜗:KWWOҠ&Y2f7cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘g*3fF5LbN2#Tf=C`!ZGUe꣇e2V<1mkS4iϗ*.{N8Xaj~ڀnAx,%fE:|YDVj ¢lg6(:k~MM5?4 ]WO>诋WZiG|QGJeK[YcյpmjE\f/ǎ8&OQ3 .3tt2'-V8pXSrY#J!Q ",ub@FK:u^iy[]<.Cw+W\)b kr-.MtڀMqʄ۰#$^X$"V`T4m~w%Pp1|+&UxY8*r8:k7QЃҀT$Ўƙ S>~Sjs:5q.w&_Z.X=:ވbw` _kd{'0:ds#qi!224nq\9-KUTsSUuVo@;Uz>^=Np>oPO @I@'Gj5o*U>^*ew>ͫʧ᫠Q5 ̈́<$#5Jٻj6e)_ d]2B:^(*:8JYS鬆Kݗ ]U4_rj{5ׇaǑ/yV?GtGb@xPU7O3|鍪 IQ5QGw *(;wf0*PUU<YƔvbt5{2!,}Ҧ:)j2OkΪ' ֊0I.q\(%ojQĖՇa<ԍexAgt'[d;׸`rcdjPFU$UeJI6T&Z}z(z vfuz {}ۿߝݞlxUZ謊.Y岟b%nw@ǩS9|źs%>_o#9\EU~/ځt(r[QZuOo;!MrU]0TcpDő?.cPuF;L_Sb}R/J_+h2$ai UǩS9>Є}76rzu~国4oĨ 1J ^̘~iC޸55G׹]gwsn zTuO=?/zƲc>Οb#7ֻcgkޛTUj*-T=]uu}>ݨNЭ [ ]:%/_ Sz]6D.mD7Uƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1c>J4hPP+A;'G_XKmL5I.},wFFum$S-E-;Õ C3I-`BRx1ғTJݕ;hΊ8 DYJo;Yš5MKɰM;%Pd9KhnD[zgVh,'C p!^M(WK2X>UQ%^p8 ˽^#Ζ؄+.@gCz%ɔ-Pr KX n>=ՔѨeSvRLz5%9UQS \WիK'hp)ô Jrh M0F (f_R5///G+x 1"eS 5 :Tf=+7Qɧ\TEs༬rYs8&k#pSՊ5MTbD܊[Ng5Q\s5PB@[8ɨV1&4Wsy[Ǿ wU2V77jމd^~YfC_h;a.&M i UWpzs`>/"'OI۲y:BzdTq£=йb:"m/-/PWDQǴ͐57m`H%AV!Hԛ׿@"Qzދ|ߒT-*OU^Ҧ6!Cwk|h&Hd5LEYy'ƣ7%*{=)Z%ٝP *G]/8Lw$?8M)\į/#7Ufd7'6\h1 vIfEIr=1w\WKVZHKgZ͡$mx % `j}TuTQJZ*H>*QxkLFTyU-)ôbiA|q`F'+ 4^Qy xH)#t^?@]^`ARSqjgB:rK۷l<2-4YKhgQLxVwP~M Φ0l 3ƅaŊITȀhwJmxIMչ|U7xˆS~2ߕ?kW1kC3];YnSґAeXYz8,'x< k7Kx]$x$vgT#w;o@ z_Vmn|HֵhZg-^TAn- )@4[*9xKƋj>!,Vt:eqn8%ohS(2\Q^aigF3vTUDVlQꅧWc%Ueq4ҝº/U $_Q!>t| ,țG<tC[xTXmf|Q%d#jUՆ|; H[bά#,Ws7NT1~m&ǻ{' \㟾 bBKJo8%!$Qj:/RX)$Sy޳ 䍧RDUg_D軦J\jN֖SU;~?Ohssdƣ}6(T <_4b5 ^N N%8QejF7toMyө`)g[/|?өJuGL坕/=CTܠhdifHcǞG4,`D՞{'xG_p/5@m +$jVH3a"*ũ,,HJҵȸT^Qyo&IÉJUVwWLeM~3tA6rwɤ6տ \0HL%LX5c@HHÃZ|NV+7WM{cig*ȸU7iÉбzd * ?gtX8̝OX:]2ɍ]p^++>AVڛE{ DB.&/56ArxY#ܕy)cKQtȪ~! ;C}ʃtf{6$NVsj wupZ)zŁ|-wg+nMVj/d+U~ͯi:_ix whqr>駃-x뼬)ݷyR=! ì:J/lIkV@n74758Z KJ(Uxz1w)^\ԣzȪ󲦨c2f؍v+6f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘2N oC\F1ִ UZJV̚\4Mgq1z{&YT ,HX~D u\g}x>+YdN̮ol ZX+F[/j+S~2/jV8Jr^ԉ]J}J*ۏ<2԰&JݣjOM@ѯ#0O[SXB^ uze\]dd./xXE f'vO_H${%;kt7ށmő|d{aފ^ǛڎE5ʋBr]W=_SAf(0 oU5q ,_\luz˪uz㻲o=Yi~| 0+=VJت /ލzM\zCL[U:|k*^8"\Wٚ\ .XTjX5 SkFu\1 q'mģ/QUؕ*AɽDNZ׮?_[# ˍ4:^j|5LG ||øBW{6[uQF.1$qF9IHg)\5>C#uXZ$#*<ߐsRv1Tj>Jm>*#( [Fhsש5*jQʼ&&&P犛L[Q1* ;X}Iΰ[Q?qQZ Hݙ֞VEsBCZ9JTK tup˷ /O,.kUdsOHMg4=-)+ؿh2Nw/r|WQn=GIU;'j,vfdzpe$V GTYsBZO1pj:r"nTUSCgr veAۘ˜FC+Ֆ#[JTe'v9-3 Dmӻuuz?0 o hxuY &_54=f07kלU0]D:jdw/+PGUVS<\2uatc^zYRąmC+7#,|:iNw*|^sm|X>Ъ^1\#͹ &%{,2U>ݎ.c05z# ogNO+Q쓭 ,˗-%K\[S_`y+b_94"U+Ύap}I[M,B.NtwHj漬E L߀ 0DX(kڵ NoU{gquz RwkէRx'uZ[3'zyyד%sƕ3jYF\s=m1&VAɼ?k\+]6yモ1gtOIW7al|1 >$]e 7؝WIe?ަL#>| ҭ] pM5MUdI61ԠeǼYGhOn3խR:^k_'Yuuq#p# J2xl>OjcY馃!ڡ+sZ/ D}2AY mpc#<'xSKx`*W[,e|6BH)㶤kjpDU(2qzx9*tqa/, Z[ 0>Ө֜xN)fă@qըFU՝w(a;ˋ>|Tc|w2eiT]*!_\WG{ ]^݅Z5t|6oYHaO@= my^akE.uz]#٥hWv(:,6A߉JFa\ wWex>vetuMYA>).,;ɦCbwjE)W Fӫ@s4e6^Q9oI}4x<.B?B߫#$Hx.x9,a!RTpgd5xBe.L7@* AsduttSVUaRU|I xG߃$T񭟬#_IFMŒ_X@foQIDII?|%$r {ENĸwޕqq?Dؽ}}o/`ӣCTi /ywO rD 9YUD] Ή@s]+'UaL} hrU'7:sU|k)H@hNq#ϵ8y˭Xű#w 1!흉R'7fuד0p!WÖW+Nmp\-ioD$g٠˅%%ÐmV]̱rw*Z}y+L Nouj}xt)lStuqxmNyKUOnDbhf}k>6ufT%{ <񐮸mjFcmUïc;w8@dGFUA& =nq5]iP}z:k⼶-ʓ Κl*'UzaxWFdZzTNRs+# wzgi:MBqtM l#^'Gߣ*^t{=rERnQ$adJl02%Tڊ^<~g?Of*U^?:N+o[PUs|QR']V-L)H K䐞 mYn\4}YVD hR;g-'3aסM Dh}1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌ3f̘1cƌk*Ț4`L$b U4\dt'>HȄ|.+Y+/Gy2OCWv3v,'kia W O6߯E=Hv $LlxI躍/}^]x\3 ɮ5 QT&G9Ay^i}O[5ޱwq4,s JJI.myE^%'VB~dׯ}*j* ~uTk\fKЬ*Y]_v'I˨鑩6Xo'j&uɧngT]oڌ9\*wVHӖ| >:5EF'J ɝ`!A e~_;5ױϊ镋m_&OVi<}"靍hW9X6KPƣ G"ƭ?/O^hCHLciPj)}QQզ#tMg9 xGw~d;_J+RỲ<;e 5/Qs/5N[!a+NPb+ѺI}-t_qU=MKʞY5no*vvbʊ{]| ~ Z{-끇^FVviϵ3Ya=6ndS;-ʹ^;uꪪ^ |=_w+"i&4l#wir|W3U$"J~O@]~tRJVMHw:̦@?>O?vdrtS*$&~1>Z}^nL(]f*&*QaIꝄ|3*O?r?*4Gyz[k/tkQϖWCCKk/x5|S*`ϹγQEwy o KYqTb$-/PtsZNKQ*>ݢU@Џ"JQ;¹& Lx;+T /+O赟> (T?ķD^N*'p$IW֐W~ =J|_UTe7ְP`;CYjk=sU[mߙ-;};2|wo1p0~>0m @Jrǟcٷ4͜?q\UUIV?2L/+Шꄾ< ܇^T ?tj\JrҀB*=km X,n}aՒIadp׷ll{\6v8RꅟҲf1F|Տ;e=\D ,D:ψrxQT◎*|{nS 9~=}ӕG~%j:Dj<ឫ:jO% $T8!jvm|'OЗ¹➱z\vsIv`Ȕʨj-^$-^G Q{m`T#c֞㸝|n.ߪN$O JUVʼt,jg-mסּNV z:(Ι*|1Ux=Yk*t MNNDUhK ؞X(刄Rv!#B_cxRŹoE5Dg>?fXQQ˔|@"աMveC>mO$H#]Y I=)_`k* :a>!X!W^wҒl'<;vwgIt_?Jh`#E:fdx=6Wu<Ӌd2di˂c#h¬c4?<HFYoVpN;ݷJ\ >` (t3{>⦊;;qFx4YcS$w.da*k|Q,+xs^K߫P^nO֮L5mIwl?-.ʲJ8 F B.-:2Ȕ!/A#b_m%I($|PZ[1G{^#o>3mw?'cx[^:Wk/`'=~֥W(gQbfv7UzM3+؍K:4|GCtA+Kʨ{@Ɩ [05E|yn4MIENDB`PKOIGCI"I"/wafer/static/img/glyphicons-halflings-white.pngPNG  IHDRӳ{PLTEmmmⰰᒒttt󻻻bbbeeeggg𶶶xxx󛛛Ƽ몪֢UUU鿿rOtRNS#_ /oS?C kDOS_6>4!~a @1_'onҋM3BQjp&%!l"Xqr; A[<`am}43/0IPCM!6(*gK&YQGDP,`{VP-x)h7e1]W$1bzSܕcO]U;Zi'y"؆K 64Y*.v@c.};tN%DI !ZЏ5LH26 ɯ" -bE,,)ʏ B>mn6pmRO wm@V#?'CȑZ#qb|$:)/E%nRqChn%i̓}lm ?idd",`H"r.z~(bQU&)5X#EMR<*p[[%.Ọk7lIoJF lV!̡ăuH`&,zRk$|$lXbjߪdU?Σ$HW$U'HE3*խU\}( zhVk}guRk$%|T|ck獳"D_W+.Q)@ƽHbslTDR2Xm#a 3lYzj㒚#! 4J8(cvt]aT D ΅Q?^-_^$:\V $N|=(vZ'q6Z׆B5V!y3K㱿bv4xR]al!IoP@tVyL٪mlڿIUb|[*lke'*WddDӝ}\W_WߝrN?vޫ۲X%0uoui*JVƦb%}i5IYlNE-wςf_W3mI-mQ)S kTC7m<"܌bT|'$ҘR&>O p6tSN\ׯLm\r@3uT b7t.5.q3r0=8TiJ\6uF R32^'ŪxI F8O{%8kJMSȴdBEdWCYO:/ON/I_=xFE! =i:o~ y?''[͓[͓[͓[͓[ͭ.U>$PƦc%]\c:| ,eSZ,oXrX!R@Zv 0>?* <|N60;{ad2v+D^t[q!۞V}fۨϏYeॗ)Vyl|" fUq@Ǽ4Y-Y-!6aB:o%JIUQ|UKO`=\ :0x Pau@!KPdxhw1>$j΍vZdxSUA&[URd7øzk/rU^w:I.VǮc>q.!zSr&2)Wg R -iQ 8Pa\ОU%iݡU_=p Lu(N?0?Æ:]άtB%U|NsorNf ,P !v" Y6hL_@@bscqgv4||0lϟ$S9bʱj#~?o}}7sAPm:IV=n !{{hEࢪ8suoLT$;VscqD3 ༂3.DBB4&V' T `D6Ϸqyj8V*X%@s\jrN$|=5Ά 'mUiKi%CI:ssaƅ`*`=l)>u՘MeuSI_OL_}o&jzp{lu:O)s%Q@$<]f xO%PCbhr2PKpf5Në3^o]eJiB464^tuٲU֌:G4'22YpuG'/Py4?.SBP_>I 1t3ΓBɭɭɭɭVVVVVs]!67(g y@ 4>Q VF}^Xׇڼje26 L%YGh lC})< !EEPZWZV+@†R 5{@ouɐ4&H6ey V݀VťcqZޒrJyByFzFN$Hb*+jՏqэ ګkݿUXle1d0d^-B%} {Y%r*j5Ak5u",:~ҸY~ hSA~6 fulՇf{ȵQtATHZkƭ/_Sn u']b]|m`BāJ,O$du]Zs FL:aǙT4o~by?wpj滥A(x]†f~an֧/^dڲcՇ,!1i&xi_VK@ip̓9Vi%a; L?0J*Ū5U'x^6V[^ {eU|:0=0d۫o*Jq%[YN.sQLud[29I:WnmXlڃ6!lNlVէKUjV\J%UߊBLcKfb>a=b~R]aG%[js@/9MطݘU>yɲX@} Ftg^vO\Ӹwvpz3K5i!$P>ā'VƛL2r@UMKZ6tw맟¦bm1h||]}~0MjA(JJP68C&yr׉e}j_cJ?I0k>šW |Bޝ."TEXd 8!cw*E(J)![W"j_ТeX_XB;oO0~?:PC (.[!Wq%*leY)E<^KZT60.#A\5;Rmtkd/8)5~^0 #Ckgey)ͶԺ6ĥ<(?&uAVm0^h.txR*a':,H|ō l5z;8+e#b'#|}2w(|KcJ l6 w^Տoi3H R ̔9,YgPְ:N [5SR![)]i}`mN4Хv`|;f(FltL8÷Z#AO%Y)NU5YedJE3dZذݣHT1 ;8MjnʏӤqp 1h^<<>yt{?|'j)}YUU{@V/J1F+7䀉[OWO[ yUY!?BD%DWj>-Ai6xz)U R7 d@g\so)a4zf[W+> P> |qLG8vȣlj2Zt+VA6gT *ʆUz(m)CD `He/.:zN9pgo &NC׃އ>Wհ_Hj)Xe6F7pm-`'c.AZ=^e8F;{Rtn(z!S7o Iew3]bܗ85|iϠRJkʱZRO+8U&:]ZieR(JMޗ7Z@5a^\GzsρU*rMezT^:ɬͦX=>$ bi>U&XQoybbGk8 Ҙn).Սo ^MmdZi$soo*{4eLbLٳ""mx:`:mk[geTެ)'0*TB{!I ''''[͓[͓[͓[͓[]Zj Q.e '/yvQ71(Z&X?(_Z){tڀmZWϏ)-C jqn,̋"IvUL!h꛿skAcrN佚фVE40yX~4zʸV㳰%,)fqtpu~  *^0:ܲ33JO(ZB?K^ v]unlWi0p6[착C_5X#[wX3b廫R{NKAe Se|wxso>P\儔ԕ6;nVmfI$V͓J-J%֌0UwYЎSnum藮xz˗VƫIvnW_qLZ"_Xz 8]Ap?C543zw({7e*Ȳ`۰!AQ:KUnz]1yVGaCm0PY ٚUx6TT&hV9V ӬzÑ 1[XzZ9erqJND/gX*9oN6D` {I%Mz9—TQ7f\"j_3~xB'ܷY]*KЌ%"5"qxq~ƕ=jS>jV&~]2xzF1X_yD<#NRB}K/iy !V^˿eJ}/FkA7 S+.(ecJ:zWZ몖wQ~ä́p6,e5,+,tv%O^OO}ן -O7>ekC6wa_C |9*WA)UJg8=:mjUvqysܒLglC6+[FSWg9wV31A ND<$5e(s[ ۨbaF.]KIENDB`PK}Hl wafer/templates/wafer/nav.html{% load i18n %} PKOIGxBwii wafer/templates/wafer/index.html{% extends "wafer/base.html" %} {% block content %}

    Create a Page called 'index'

    {% endblock %} PK(H"gwafer/templates/wafer/base.html{% load sponsors %} {{ WAFER_CONFERENCE_NAME }} {% block extra_head %}{% endblock %} {% include "wafer/nav.html" %}
    {% if messages %}
    {% for message in messages %}
    {{ message }}
    {% endfor %}
    {% endif %} {% block content %}

    {{ WAFER_CONFERENCE_NAME }}

    {% endblock %}
    {% sponsors %} {% block extra_foot %}{% endblock %} PKOIG<wafer/management/static.pyimport shutil import os from django_medusa.renderers import DiskStaticSiteRenderer from django.conf import settings class WaferDiskStaticSiteRenderer(DiskStaticSiteRenderer): # People may create pages that mirror directories - if this happens, # we skip the page, rather than aborting and print a message, since # this may require manual fixup later def render_path(self, path=None, view=None): if not path: super(WaferDiskStaticSiteRenderer, self).render_path(path, view) else: try: super(WaferDiskStaticSiteRenderer, self).render_path(path, view) except IOError as err: print('Skiping %s - threw IOError %s' % (path, err)) # This is a hack because dajngo_medusa doens't understand 301 if path == '/': DEPLOY_DIR = settings.MEDUSA_DEPLOY_DIR # Also copy to index, as specified by the pages url outpath = os.path.join(DEPLOY_DIR, 'index') inpath = os.path.join(DEPLOY_DIR, 'index.html') shutil.copyfile(inpath, outpath) PKOIGwafer/management/__init__.pyPKcG||2wafer/management/commands/wafer_speaker_tickets.pyimport sys import csv from optparse import make_option from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from wafer.talks.models import ACCEPTED class Command(BaseCommand): help = ("List speakers and associated tickets. By default, only lists" " speakers for accepted talk, but this can be overriden by" " the --all option") option_list = BaseCommand.option_list + tuple([ make_option('--all', action="store_true", default=False, help='List speakers and tickets (for all talks)'), ]) def _speaker_tickets(self, options): people = get_user_model().objects.filter( talks__isnull=False).distinct() csv_file = csv.writer(sys.stdout) for person in people: # We query talks to filter out the speakers from ordinary # accounts if options['all']: titles = [x.title for x in person.talks.all()] else: titles = [x.title for x in person.talks.filter(status=ACCEPTED)] if not titles: continue tickets = person.ticket.all() if tickets: ticket = u'%d' % tickets[0].barcode else: ticket = u'NO TICKET PURCHASED' row = [x.encode("utf-8") for x in ( person.userprofile.display_name(), person.email, ticket, )] csv_file.writerow(row) def handle(self, *args, **options): self._speaker_tickets(options) PK(H]6O O 7wafer/management/commands/wafer_registered_attendees.pyimport codecs import sys from django.conf import settings from django.contrib.auth.models import Group from django.core.management.base import BaseCommand from django.utils.module_loading import import_string from wafer.users.models import UserProfile if sys.version_info >= (3,): import csv else: from backports import csv class RegisteredUserList(object): def fields(self): return ('username', 'name', 'email') def details(self, person): user = person.user return ( user.username, person.display_name(), user.email, ) def attendees(self): people = UserProfile.objects.all().order_by( 'user__username').prefetch_related('user', 'kv') for person in people: if person.is_registered(): yield self.details(person) class TicketRegisteredUserList(RegisteredUserList): def fields(self): return super(TicketRegisteredUserList, self).fields() + ( 'ticket_type', 'ticket_barcode') def details(self, person): ticket = person.user.ticket.first() details = (None, None) if ticket: details = (ticket.type.name, ticket.barcode) return super(TicketRegisteredUserList, self).details(person) + details class FormRegisteredUserList(RegisteredUserList): def __init__(self): self.group = Group.objects.get_by_natural_key('Registration') form_class = import_string(settings.WAFER_REGISTRATION_FORM) self.form = form_class() def fields(self): return super(FormRegisteredUserList, self).fields() + tuple( self.form.fields.keys()) def _iter_details(self, registration_data): for field in self.form.fields.keys(): item = registration_data.filter(key=field).first() if item: yield item.value else: yield None def details(self, person): registration_data = person.kv.filter(group=self.group) details = tuple(self._iter_details(registration_data)) return super(FormRegisteredUserList, self).details(person) + details class Command(BaseCommand): help = "Dump attendee registration information" def handle(self, *args, **options): stream_writer = codecs.getwriter('utf-8') bytestream = getattr(sys.stdout, 'buffer', sys.stdout) csv_file = csv.writer(stream_writer(bytestream)) if settings.WAFER_REGISTRATION_MODE == 'ticket': user_list = TicketRegisteredUserList() elif settings.WAFER_REGISTRATION_MODE == 'form': user_list = FormRegisteredUserList() else: raise NotImplemented('Unknown WAFER_REGISTRATION_MODE') csv_file.writerow(user_list.fields()) for row in user_list.attendees(): csv_file.writerow(row) PKOIG%wafer/management/commands/__init__.pyPKcG1o53 3 )wafer/management/commands/wafer_emails.pyimport sys import csv from optparse import make_option from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from wafer.talks.models import ACCEPTED class Command(BaseCommand): help = "List author or web-site user email addresses." option_list = BaseCommand.option_list + tuple([ make_option('--authors', action="store_true", default=False, help='List author email addresses only' ' (for accepted talks)'), make_option('--allauthors', action="store_true", default=False, help='List author emails only (for all talks)'), make_option('--speakers', action="store_true", default=False, help='List speaker email addresses' ' (for accepted talks)'), make_option('--allspeakers', action="store_true", default=False, help='List speaker email addresses' ' (for all talks)'), ]) def _website_user_emails(self, options): query = {} people = get_user_model().objects.all() csv_file = csv.writer(sys.stdout) for person in people: row = [x.encode("utf-8") for x in (person.username, person.get_full_name(), person.email)] csv_file.writerow(row) def _author_emails(self, options): # Should grow more options - accepted talks, under consideration, etc. people = get_user_model().objects.filter( contact_talks__isnull=False).distinct() csv_file = csv.writer(sys.stdout) for person in people: if options['allauthors']: titles = [x.title for x in person.contact_talks.all()] else: titles = [x.title for x in person.contact_talks.filter(status=ACCEPTED)] if not titles: continue row = [x.encode("utf-8") for x in (person.userprofile.display_name(), person.email, ';'.join(titles))] csv_file.writerow(row) def _speaker_emails(self, options): people = get_user_model().objects.filter(talks__isnull=False).distinct() csv_file = csv.writer(sys.stdout) for person in people: if options['allspeakers']: titles = [x.title for x in person.talks.all()] else: titles = [x.title for x in person.talks.filter(status=ACCEPTED)] if not titles: continue row = [x.encode("utf-8") for x in (person.userprofile.display_name(), person.email, ';'.join(titles))] csv_file.writerow(row) def handle(self, *args, **options): if options['authors'] or options['allauthors']: self._author_emails(options) elif options['speakers'] or options['allspeakers']: self._speaker_emails(options) else: self._website_user_emails(options) PKOIG22(wafer/management/commands/wafer_stats.pyfrom django.core.management.base import BaseCommand from django.contrib.auth import get_user_model class Command(BaseCommand): help = "Misc stats." option_list = BaseCommand.option_list + tuple([ ]) def _speakers(self, *args, **kwargs): return get_user_model().objects.filter( contact_talks__isnull=False).filter(*args, **kwargs).count() def handle(self, *args, **options): print("Speakers:\n") # FIXME: more stats - accepted, rejected, pending, etc. print(" Total: %s" % self._speakers()) PKcG:wafer/management/commands/wafer_speaker_contact_details.pyimport sys import csv from optparse import make_option from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model from wafer.talks.models import ACCEPTED class Command(BaseCommand): help = "List contact details for the speakers." option_list = BaseCommand.option_list + tuple([ make_option('--speakers', action="store_true", default=False, help='List speaker email addresses' ' (for accepted talks)'), make_option('--allspeakers', action="store_true", default=False, help='List speaker email addresses' ' (for all talks)'), ]) def _speaker_emails(self, options): people = get_user_model().objects.filter( talks__isnull=False).distinct() csv_file = csv.writer(sys.stdout) for person in people: if options['allspeakers']: titles = [x.title for x in person.talks.all()] else: titles = [x.title for x in person.talks.filter(status=ACCEPTED)] if not titles: continue row = [x.encode("utf-8") for x in (person.userprofile.display_name(), person.email, person.userprofile.contact_number or 'NO CONTACT INFO', ';'.join(titles))] csv_file.writerow(row) def handle(self, *args, **options): self._speaker_emails(options) PK}HwgQ,,5wafer/management/commands/wafer_add_default_groups.py# -*- coding: utf-8 -*- from django.core.management.base import BaseCommand from django.contrib.auth.models import Group, Permission class Command(BaseCommand): help = "Add some useful default groups" option_list = BaseCommand.option_list + tuple([ ]) GROUPS = { # Permissions are specified as (app, code_name) pairs 'Page Editors': ( ('pages', 'add_page'), ('pages', 'delete_page'), ('pages', 'change_page'), ('pages', 'add_file'), ('pages', 'delete_file'), ('pages', 'change_file'), ), 'Talk Mentors': ( ('talks', 'change_talk'), ('talks', 'view_all_talks'), ), 'Registration': (), } def add_wafer_groups(self): # This creates the groups we need for page editor and talk mentor # roles. for wafer_group, permission_list in self.GROUPS.items(): group, created = Group.objects.all().get_or_create( name=wafer_group) if not created: print('Using existing %s group' % wafer_group) for app, perm_code in permission_list: try: perm = Permission.objects.filter( codename=perm_code, content_type__app_label=app).get() except Permission.DoesNotExist: print('Unable to find permission %s' % perm_code) continue except Permission.MultipleObjectsReturned: print('Non-unique permission %s' % perm_code) if perm not in group.permissions.all(): print('Adding %s to %s' % (perm_code, wafer_group)) group.permissions.add(perm) group.save() def handle(self, *args, **options): self.add_wafer_groups() PK}H}Awafer/talks/urls.pyfrom django.conf.urls import patterns, url, include from rest_framework import routers from wafer.talks.views import ( Speakers, TalkCreate, TalkDelete, TalkUpdate, TalkView, UsersTalks, TalksViewSet) router = routers.DefaultRouter() router.register(r'talks', TalksViewSet) urlpatterns = patterns( '', url(r'^$', UsersTalks.as_view(), name='wafer_users_talks'), url(r'^page/(?P\d+)$', UsersTalks.as_view(), name='wafer_users_talks_page'), url(r'^new/$', TalkCreate.as_view(), name='wafer_talk_submit'), url(r'^(?P\d+)/$', TalkView.as_view(), name='wafer_talk'), url(r'^(?P\d+)/edit/$', TalkUpdate.as_view(), name='wafer_talk_edit'), url(r'^(?P\d+)/delete/$', TalkDelete.as_view(), name='wafer_talk_delete'), url(r'^speakers/$', Speakers.as_view(), name='wafer_talks_speakers'), url(r'^api/', include(router.urls)), ) PKyImSwafer/talks/views.pyfrom django.core.exceptions import PermissionDenied, ValidationError from django.core.urlresolvers import reverse_lazy from django.http import HttpResponseRedirect from django.views.generic import DetailView from django.views.generic.edit import CreateView, UpdateView, DeleteView from django.views.generic.list import ListView from django.conf import settings from django.db.models import Q from reversion import revisions from rest_framework import viewsets from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly from wafer.utils import LoginRequiredMixin from wafer.talks.models import Talk, TalkType, ACCEPTED from wafer.talks.forms import get_talk_form_class from wafer.talks.serializers import TalkSerializer from wafer.users.models import UserProfile class EditOwnTalksMixin(object): '''Users can edit their own talks as long as the talk is "Under Consideration"''' def get_object(self, *args, **kwargs): object_ = super(EditOwnTalksMixin, self).get_object(*args, **kwargs) if object_.can_edit(self.request.user): return object_ else: raise PermissionDenied class UsersTalks(ListView): template_name = 'wafer.talks/talks.html' paginate_by = 25 def get_queryset(self): # self.request will be None when we come here via the static site # renderer if (self.request and Talk.can_view_all(self.request.user)): return Talk.objects.all() return Talk.objects.filter(status=ACCEPTED) class TalkView(DetailView): template_name = 'wafer.talks/talk.html' model = Talk def get_object(self, *args, **kwargs): '''Only talk owners can see talks, unless they've been accepted''' object_ = super(TalkView, self).get_object(*args, **kwargs) if object_.can_view(self.request.user): return object_ else: raise PermissionDenied def get_context_data(self, **kwargs): context = super(TalkView, self).get_context_data(**kwargs) context['can_edit'] = self.object.can_edit(self.request.user) return context class TalkCreate(LoginRequiredMixin, CreateView): model = Talk template_name = 'wafer.talks/talk_form.html' def get_form_class(self): return get_talk_form_class() def get_form_kwargs(self): kwargs = super(TalkCreate, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs def get_context_data(self, **kwargs): context = super(TalkCreate, self).get_context_data(**kwargs) can_submit = getattr(settings, 'WAFER_TALKS_OPEN', True) if can_submit: # Check for all talk types being disabled can_submit = TalkType.objects.filter(disable_submission=False).count() > 0 context['can_submit'] = can_submit return context @revisions.create_revision() def form_valid(self, form): if not getattr(settings, 'WAFER_TALKS_OPEN', True): # Should this be SuspiciousOperation? raise ValidationError("Talk submission isn't open") # Eaaargh we have to do the work of CreateView if we want to set values # before saving self.object = form.save(commit=False) self.object.corresponding_author = self.request.user self.object.save() revisions.set_user(self.request.user) revisions.set_comment("Talk Created") # Save the author information as well (many-to-many fun) form.save_m2m() return HttpResponseRedirect(self.get_success_url()) class TalkUpdate(EditOwnTalksMixin, UpdateView): model = Talk template_name = 'wafer.talks/talk_form.html' def get_form_class(self): return get_talk_form_class() def get_form_kwargs(self): kwargs = super(TalkUpdate, self).get_form_kwargs() kwargs['user'] = self.request.user return kwargs def get_context_data(self, **kwargs): context = super(TalkUpdate, self).get_context_data(**kwargs) context['can_edit'] = self.object.can_edit(self.request.user) return context @revisions.create_revision() def form_valid(self, form): revisions.set_user(self.request.user) revisions.set_comment("Talk Modified") return super(TalkUpdate, self).form_valid(form) class TalkDelete(EditOwnTalksMixin, DeleteView): model = Talk template_name = 'wafer.talks/talk_delete.html' success_url = reverse_lazy('wafer_page', args=('index',)) @revisions.create_revision() def form_valid(self, form): # We don't add any metadata, as the admin site # doesn't show it for deleted talks. return super(TalkDelete, self).form_valid(form) class Speakers(ListView): model = Talk template_name = 'wafer.talks/speakers.html' def _by_row(self, speakers, n): return [speakers[i:i + n] for i in range(0, len(speakers), n)] def get_context_data(self, **kwargs): context = super(Speakers, self).get_context_data(**kwargs) speakers = UserProfile.objects.filter( user__talks__status='A').distinct().prefetch_related('user').order_by('user__first_name', 'user__last_name') context["speaker_rows"] = self._by_row(speakers, 4) return context class TalksViewSet(viewsets.ModelViewSet): """API endpoint that allows talks to be viewed or edited.""" queryset = Talk.objects.none() # Needed for the REST Permissions serializer_class = TalkSerializer # XXX: Do we want to allow authors to edit talks via the API? permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) def get_queryset(self): # We override the default implementation to only show accepted talks # to people who aren't part of the management group if self.request.user.id is None: # Anonymous user, so just accepted talks return Talk.objects.filter(status=ACCEPTED) elif Talk.can_view_all(self.request.user): return Talk.objects.all() else: # Also include talks owned by the user # XXX: Should this be all authors rather than just # the corresponding author? return Talk.objects.filter( Q(status=ACCEPTED)| Q(corresponding_author=self.request.user)) PKyIcJ| | wafer/talks/forms.pyimport copy from django import forms from django.conf import settings from django.db.models import Q from django.core.urlresolvers import reverse from django.utils.module_loading import import_string from django.utils.translation import ugettext as _ from crispy_forms.bootstrap import FormActions from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit, HTML from markitup.widgets import MarkItUpWidget from easy_select2.widgets import Select2Multiple from wafer.talks.models import Talk, TalkType, render_author def get_talk_form_class(): return import_string(settings.WAFER_TALK_FORM) class TalkForm(forms.ModelForm): def __init__(self, *args, **kwargs): self.user = kwargs.pop('user') initial = kwargs.setdefault('initial', {}) if kwargs['instance']: authors = kwargs['instance'].authors.all() else: authors = initial['authors'] = [self.user] if not (settings.WAFER_PUBLIC_ATTENDEE_LIST or self.user.has_perm('talks.change_talk')): # copy base_fields because it's a shared class attribute self.base_fields = copy.deepcopy(self.base_fields) self.base_fields['authors'].limit_choices_to = { 'id__in': [author.id for author in authors]} super(TalkForm, self).__init__(*args, **kwargs) if not self.user.has_perm('talks.edit_private_notes'): self.fields.pop('private_notes') # We add the name, if known, to the authors list self.fields['authors'].label_from_instance = render_author self.helper = FormHelper(self) submit_button = Submit('submit', _('Submit')) instance = kwargs['instance'] if instance: self.helper.layout.append( FormActions( submit_button, HTML('%s' % (reverse('wafer_talk_delete', args=(instance.pk,)), _('Delete'))))) else: self.helper.add_input(submit_button) # Exclude disabled talk types from the choice widget if kwargs['instance'] and kwargs['instance'].talk_type: # Ensure the current talk type is in the query_set, regardless of whether it's been disabled since then self.fields['talk_type'].queryset = TalkType.objects.filter(Q(disable_submission=False) | Q(pk=kwargs['instance'].talk_type.pk)) else: self.fields['talk_type'].queryset = TalkType.objects.filter(disable_submission=False) class Meta: model = Talk fields = ('title', 'talk_type', 'abstract', 'authors', 'notes', 'private_notes') widgets = { 'abstract': MarkItUpWidget(), 'notes': forms.Textarea(attrs={'class': 'input-xxlarge'}), 'authors': Select2Multiple(), } PKyI4 wafer/talks/models.pyfrom django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.conf import settings from django.db import models from django.utils.encoding import python_2_unicode_compatible from django.template.defaultfilters import slugify from markitup.fields import MarkupField from wafer.kv.models import KeyValue # constants to make things clearer elsewhere ACCEPTED = 'A' PENDING = 'P' REJECTED = 'R' CANCELLED = 'C' # Utility functions used in the forms def render_author(author): return '%s (%s)' % (author.userprofile.display_name(), author) @python_2_unicode_compatible class TalkType(models.Model): """A type of talk.""" name = models.CharField(max_length=255) description = models.TextField(max_length=1024) order = models.IntegerField(default=1) disable_submission = models.BooleanField(default=False, help_text="Don't allow users to submit talks of this type.") def __str__(self): return u'%s' % (self.name,) class Meta: ordering = ['order', 'id'] def css_class(self): """Return a string for use as a css class name""" # While css can represent complicated strings # using escaping, we want simplicity and obvious predictablity return u'talk-type-%s' % slugify(self.name) css_class.admin_order_field = 'name' css_class.short_description = 'CSS class name' @python_2_unicode_compatible class Talk(models.Model): class Meta: permissions = ( ("view_all_talks", "Can see all talks"), ("edit_private_notes", "Can edit the private notes fields"), ) TALK_STATUS = ( (ACCEPTED, 'Accepted'), (REJECTED, 'Not Accepted'), (CANCELLED, 'Talk Cancelled'), (PENDING, 'Under Consideration'), ) talk_id = models.AutoField(primary_key=True) talk_type = models.ForeignKey(TalkType, null=True) title = models.CharField(max_length=1024) abstract = MarkupField( help_text=_("Write two or three paragraphs describing your talk. " "Who is your audience? What will they get out of it? " "What will you cover?
    " "You can use Markdown syntax.")) notes = models.TextField( null=True, blank=True, help_text=_("Any notes for the conference organisers?")) private_notes = models.TextField( null=True, blank=True, help_text=_("Note space for the conference organisers (not visible " "to submitter)")) status = models.CharField(max_length=1, choices=TALK_STATUS, default=PENDING) corresponding_author = models.ForeignKey( settings.AUTH_USER_MODEL, related_name='contact_talks', help_text=_( "The person submitting the talk (and who questions regarding the " "talk should be addressed to).")) authors = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='talks', help_text=_( "The speakers presenting the talk.")) kv = models.ManyToManyField(KeyValue) def __str__(self): return u'%s: %s' % (self.corresponding_author, self.title) def get_absolute_url(self): return reverse('wafer_talk', args=(self.talk_id,)) def get_corresponding_author_contact(self): email = self.corresponding_author.email profile = self.corresponding_author.userprofile if profile.contact_number: contact = profile.contact_number else: # Should we wrap this in a span for styling? contact = 'NO CONTACT INFO' return '%s - %s' % (email, contact) get_corresponding_author_contact.short_description = 'Contact Details' def get_corresponding_author_name(self): return render_author(self.corresponding_author) get_corresponding_author_name.admin_order_field = 'corresponding_author' get_corresponding_author_name.short_description = 'Corresponding Author' def get_authors_display_name(self): authors = list(self.authors.all()) # Corresponding authors first authors.sort( key=lambda author: u'' if author == self.corresponding_author else author.userprofile.display_name()) names = [author.userprofile.display_name() for author in authors] if len(names) <= 2: return u' & '.join(names) return u'%s, et al.' % names[0] def get_in_schedule(self): if self.scheduleitem_set.all(): return True return False get_in_schedule.short_description = 'Added to schedule' get_in_schedule.boolean = True def has_url(self): """Test if the talk has urls associated with it""" if self.talkurl_set.all(): return True return False has_url.boolean = True # Helpful properties for the templates accepted = property(fget=lambda x: x.status == ACCEPTED) pending = property(fget=lambda x: x.status == PENDING) reject = property(fget=lambda x: x.status == REJECTED) cancelled = property(fget=lambda x: x.status == CANCELLED) def _is_among_authors(self, user): if self.corresponding_author.username == user.username: return True # not chaining with logical-or to avoid evaluation of the queryset return self.authors.filter(username=user.username).exists() def can_view(self, user): if user.has_perm('talks.view_all_talks'): return True if self._is_among_authors(user): return True if self.accepted: return True return False @classmethod def can_view_all(cls, user): return user.has_perm('talks.view_all_talks') def can_edit(self, user): if user.has_perm('talks.change_talk'): return True if self.pending: if self._is_among_authors(user): return True return False class TalkUrl(models.Model): """An url to stuff relevant to the talk - videos, slides, etc. Note that these are explicitly not intended to be exposed to the user, but exist for use by the conference organisers.""" description = models.CharField(max_length=256) url = models.URLField() talk = models.ForeignKey(Talk) PKOIGkIIwafer/talks/renderers.pyfrom django_medusa.renderers import StaticSiteRenderer from wafer.talks.models import Talk, ACCEPTED from wafer.talks.views import UsersTalks from django.core.urlresolvers import reverse class TalksRenderer(StaticSiteRenderer): def get_paths(self): paths = ["/talks/", ] items = Talk.objects.filter(status=ACCEPTED) for item in items: paths.append(item.get_absolute_url()) view = UsersTalks() view.request = None queryset = view.get_queryset() paginator = view.get_paginator(queryset, view.get_paginate_by(queryset)) for page in paginator.page_range: paths.append(reverse('wafer_users_talks_page', kwargs={'page': page})) return paths renderers = [TalksRenderer, ] PKOIGwafer/talks/__init__.pyPKyIlVVwafer/talks/admin.pyfrom django.contrib import admin from django import forms from django.utils.translation import ugettext_lazy as _ from reversion.admin import VersionAdmin from easy_select2 import select2_modelform_meta from wafer.compare.admin import CompareVersionAdmin, DateModifiedFilter from wafer.talks.models import TalkType, Talk, TalkUrl, render_author class AdminTalkForm(forms.ModelForm): Meta = select2_modelform_meta(Talk) def __init__(self, *args, **kwargs): super(AdminTalkForm, self).__init__(*args, **kwargs) self.fields['authors'].label_from_instance = render_author self.fields['corresponding_author'].label_from_instance = render_author class ScheduleListFilter(admin.SimpleListFilter): title = _('in schedule') parameter_name = 'schedule' def lookups(self, request, model_admin): return ( ('in', _('Allocated to schedule')), ('out', _('Not allocated')), ) def queryset(self, request, queryset): if self.value() == 'in': return queryset.filter(scheduleitem__isnull=False) elif self.value() == 'out': return queryset.filter(scheduleitem__isnull=True) return queryset class TalkUrlAdmin(VersionAdmin, admin.ModelAdmin): list_display = ('description', 'talk', 'url') class TalkUrlInline(admin.TabularInline): model = TalkUrl class TalkAdmin(CompareVersionAdmin, admin.ModelAdmin): list_display = ('title', 'get_corresponding_author_name', 'get_corresponding_author_contact', 'talk_type', 'get_in_schedule', 'has_url', 'status') list_editable = ('status',) list_filter = ('status', 'talk_type', ScheduleListFilter, DateModifiedFilter) exclude = ('kv',) inlines = [ TalkUrlInline, ] form = AdminTalkForm class TalkTypeAdmin(VersionAdmin, admin.ModelAdmin): list_display = ('name', 'order', 'disable_submission', 'css_class') readonly_fields = ('css_class',) admin.site.register(Talk, TalkAdmin) admin.site.register(TalkType, TalkTypeAdmin) admin.site.register(TalkUrl, TalkUrlAdmin) PK}HnUwafer/talks/serializers.pyfrom django.contrib.auth import get_user_model from rest_framework import serializers from reversion import revisions from wafer.talks.models import Talk class TalkSerializer(serializers.ModelSerializer): authors = serializers.PrimaryKeyRelatedField( many=True, allow_null=True, queryset=get_user_model().objects.all()) class Meta: model = Talk # private_notes should possibly be accessible to # talk reviewers by the API, but certainly # not to the other users. # Similar considerations apply to notes, which should # not be generally accessible exclude = ('_abstract_rendered', 'private_notes', 'notes') @revisions.create_revision() def create(self, validated_data): revisions.set_comment("Created via REST api") return super(TalkSerializer, self).create(validated_data) @revisions.create_revision() def update(self, talk, validated_data): revisions.set_comment("Changed via REST api") talk.abstract = validated_data['abstract'] talk.title = validated_data['title'] talk.talk_type = validated_data['talk_type'] talk.authors = validated_data['authors'] talk.status = validated_data['status'] # These need more thought #talk.notes = validated_data['notes'] #talk.private_notes = validated_data['private_notes'] talk.save() return talk PKyI(= +wafer/talks/templates/wafer.talks/talk.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {{ object.title }} {% if can_edit %} {% trans 'Edit' %} {% endif %}

    {% blocktrans count counter=object.authors.count %} Speaker: {% plural %} Speakers: {% endblocktrans %} {% for author in object.authors.all %} {{ author.userprofile.display_name }} {% endfor %}

    {% blocktrans with talk_type=object.talk_type.name|default:'Talk' %} Type: {{ talk_type }} {% endblocktrans %}

    {% if object.get_in_schedule %} {% for schedule in object.scheduleitem_set.all %}

    {% blocktrans with venue=schedule.venue %} Room: {{ venue }} {% endblocktrans %}

    {% blocktrans with start_time=schedule.get_start_time %} Time: {{ start_time }} {% endblocktrans %}

    {% blocktrans with hours=schedule.get_duration.hours|stringformat:"d" minutes=schedule.get_duration.minutes|stringformat:"02d" %} Duration: {{ hours }}:{{ minutes }} {% endblocktrans %}

    {% endfor %} {% endif %}
    {% if user.is_staff or perms.talks.view_all_talks %}

    {% trans 'Status:' %} {% if object.pending %} {% trans 'Under consideration' %} {% elif object.accepted %} {% trans 'Accepted' %} {% elif object.cancelled %} {% trans 'Cancelled' %} {% else %} {% trans 'Not accepted' %} {% endif %}

    {% endif %}
    {{ object.abstract.rendered|safe }}
    {% if perms.talks.view_all_talks or user.is_superuser %} {% if talk.notes %} {% blocktrans %}

    Talk Notes

    (The following is not visible to attendees.)

    {% endblocktrans %}
    {{ object.notes|urlize|linebreaks }}
    {% endif %} {% endif %} {% if perms.talks.edit_private_notes and object.private_notes %} {% blocktrans %}

    Private notes

    (The following is not visible to submitters or attendees.)

    {% endblocktrans %}
    {{ object.private_notes|urlize|linebreaks }}
    {% endif %} {% if talk.talkurl_set.all %}
    {% trans "URLS" %}
    {% endif %}
    {% endblock %} PKfGc)0wafer/talks/templates/wafer.talks/talk_form.html{% extends 'wafer/base.html' %} {% load i18n %} {% load crispy_forms_tags %} {% block content %}
    {% if can_edit %}

    {% trans "Edit Talk" %}

    {% with corresponding_author_name=object.corresponding_author.userprofile.display_name %} {% url 'wafer_user_profile' username=object.corresponding_author.username as corresponding_author_url %} {% blocktrans %}

    Submitted by {{ corresponding_author_name }}.

    {% endblocktrans %} {% endwith %} {% else %}

    {% trans "Talk Submission" %}

    {% endif %} {% if not can_edit and not can_submit %} {% blocktrans %} Talk submission is closed {% endblocktrans %} {% else %} {{ form.media }} {% crispy form %} {% endif %}
    {% endblock %} PKyI_aa/wafer/talks/templates/wafer.talks/speakers.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}
    {% for row in speaker_rows %}
    {% for user_profile in row %} {% endfor %}
    {% endfor %}
    {% endblock %} PKHmj}},wafer/talks/templates/wafer.talks/talks.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {% trans 'Talks' %}

    {% for talk in talk_list %}
    {% if talk.pending %} {{ talk.status }} {% elif talk.reject %} {{ talk.status }} {% elif talk.cancelled %} {{ talk.status }} {% endif %} {{ talk.title }} by {{ talk.get_authors_display_name }}
    {% empty %}

    No talks accepted yet.

    {% endfor %}
    {% if is_paginated %}
      {% if page_obj.has_previous %}
    • «
    • {% else %}
    • «
    • {% endif %} {% for page in paginator.page_range %}
    • {{ page }}
    • {% endfor %} {% if page_obj.has_next %}
    • »
    • {% else %}
    • »
    • {% endif %}
    {% endif %} {% endblock %} PKOIGsLnn2wafer/talks/templates/wafer.talks/talk_delete.html{% extends 'wafer/base.html' %} {% load i18n %} {% load crispy_forms_tags %} {% block content %}

    {% trans "Confirm Talk Deletion" %}

    {% blocktrans with talk_title=object.title talk_url=object.get_absolute_url %} You are about to delete the talk "{{ talk_title }}".
    This cannot be undone. {% endblocktrans %}
    {% csrf_token %}
    {% endblock %} PK}H  +wafer/talks/tests/test_wafer_basic_talks.py# This tests the very basic talk stuff, to ensure some levels of sanity def test_add_talk(): """Create a user and add a talk to it""" from django.contrib.auth import get_user_model from wafer.talks.models import Talk user = get_user_model().objects.create_user('john', 'best@wafer.test', 'johnpassword') Talk.objects.create( title="This is a test talk", abstract="This should be a long and interesting abstract, but isn't", corresponding_author_id=user.id) assert user.contact_talks.count() == 1 def test_filter_talk(): """Create a second user and check some filters""" from django.contrib.auth import get_user_model UserModel = get_user_model() UserModel.objects.create_user('james', 'best@wafer.test', 'johnpassword') assert UserModel.objects.filter(contact_talks__isnull=False).count() == 1 assert UserModel.objects.filter(contact_talks__isnull=True).count() == 1 def test_multiple_talks(): """Add more talks""" from wafer.talks.models import Talk from django.contrib.auth import get_user_model UserModel = get_user_model() user1 = UserModel.objects.filter(username='john').get() user2 = UserModel.objects.filter(username='james').get() Talk.objects.create( title="This is a another test talk", abstract="This should be a long and interesting abstract, but isn't", corresponding_author_id=user1.id) assert len([x.title for x in user1.contact_talks.all()]) == 2 assert len([x.title for x in user2.contact_talks.all()]) == 0 Talk.objects.create( title="This is a third test talk", abstract="This should be a long and interesting abstract, but isn't", corresponding_author_id=user2.id) assert len([x.title for x in user2.contact_talks.all()]) == 1 def test_corresponding_author_details(): """Create a second user and check some filters""" from django.contrib.auth import get_user_model from wafer.talks.models import Talk UserModel = get_user_model() user = UserModel.objects.create_user('jeff', 'best@wafer.test', 'johnpassword') profile = user.userprofile profile.contact_number = '77776' profile.save() speaker = UserModel.objects.create_user('bob', 'bob@wafer.test', 'bobpassword') Talk.objects.create( title="This is a another test talk", abstract="This should be a long and interesting abstract, but isn't", corresponding_author_id=user.id) talk = user.contact_talks.all()[0] talk.authors.add(user) talk.authors.add(speaker) talk.save() assert talk.get_authors_display_name() == 'jeff & bob' assert talk.get_corresponding_author_contact() == 'best@wafer.test - 77776' assert talk.get_corresponding_author_name() == 'jeff (jeff)' speaker.first_name = 'Bob' speaker.last_name = 'Robert' speaker.save() assert talk.get_authors_display_name() == 'jeff & Bob Robert' PKOIGwafer/talks/tests/__init__.pyPKyI@d:#EEwafer/talks/tests/test_views.py"""Tests for wafer.talk views.""" import mock from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.core.urlresolvers import reverse from django.test import Client, TestCase from wafer.talks.models import (Talk, ACCEPTED, REJECTED, PENDING, CANCELLED) def create_user(username, superuser=False, perms=()): if superuser: create = get_user_model().objects.create_superuser else: create = get_user_model().objects.create_user user = create( username, '%s@example.com' % username, '%s_password' % username) for codename in perms: perm = Permission.objects.get(codename=codename) user.user_permissions.add(perm) if perms: user = get_user_model().objects.get(pk=user.pk) return user def create_talk(title, status, username): user = create_user(username) talk = Talk.objects.create( title=title, status=status, corresponding_author_id=user.id) talk.authors.add(user) talk.notes = "Some notes for talk %s" % title talk.private_notes = "Some private notes for talk %s" % title talk.save() return talk def mock_avatar_url(self): if self.user.email is None: return None return "avatar-%s" % self.user.email class UsersTalksTests(TestCase): def setUp(self): self.talk_a = create_talk("Talk A", ACCEPTED, "author_a") self.talk_r = create_talk("Talk R", REJECTED, "author_r") self.talk_p = create_talk("Talk P", PENDING, "author_p") self.client = Client() def test_not_logged_in(self): """Test that unauthenticated users only see accepted talks.""" response = self.client.get('/talks/') self.assertEqual(response.status_code, 200) self.assertEqual(set(response.context['talk_list']), set([self.talk_a])) def test_admin_user(self): """Test that admin users see all talks.""" create_user('super', superuser=True) self.client.login(username='super', password='super_password') response = self.client.get('/talks/') self.assertEqual(response.status_code, 200) self.assertEqual(set(response.context['talk_list']), set([self.talk_a, self.talk_r, self.talk_p])) def test_user_with_view_all(self): """Test that users with the view_all permission see all talks.""" create_user('reviewer', perms=['view_all_talks']) self.client.login(username='reviewer', password='reviewer_password') response = self.client.get('/talks/') self.assertEqual(response.status_code, 200) self.assertEqual(set(response.context['talk_list']), set([self.talk_a, self.talk_r, self.talk_p])) class TalkViewTests(TestCase): def setUp(self): self.talk_a = create_talk("Talk A", ACCEPTED, "author_a") self.talk_r = create_talk("Talk R", REJECTED, "author_r") self.talk_p = create_talk("Talk P", PENDING, "author_p") self.talk_c = create_talk("Talk C", CANCELLED, "author_c") self.client = Client() def check_talk_view(self, talk, status_code, auth=None): if auth is not None: self.client.login(**auth) response = self.client.get( reverse('wafer_talk', kwargs={'pk': talk.pk})) self.assertEqual(response.status_code, status_code) def test_view_accepted_not_logged_in(self): self.check_talk_view(self.talk_a, 200) def test_view_rejected_not_logged_in(self): self.check_talk_view(self.talk_r, 403) def test_view_cancelled_not_logged_in(self): self.check_talk_view(self.talk_c, 403) def test_view_pending_not_logged_in(self): self.check_talk_view(self.talk_p, 403) def test_view_accepted_author(self): self.check_talk_view(self.talk_a, 200, auth={ 'username': 'author_a', 'password': 'author_a_password', }) def test_view_rejected_author(self): self.check_talk_view(self.talk_r, 200, auth={ 'username': 'author_r', 'password': 'author_r_password', }) def test_view_pending_author(self): self.check_talk_view(self.talk_p, 200, auth={ 'username': 'author_p', 'password': 'author_p_password', }) def test_view_accepted_has_view_all_perm(self): create_user('reviewer', perms=['view_all_talks']) self.check_talk_view(self.talk_a, 200, auth={ 'username': 'reviewer', 'password': 'reviewer_password', }) def test_view_rejected_has_view_all_perm(self): create_user('reviewer', perms=['view_all_talks']) self.check_talk_view(self.talk_r, 200, auth={ 'username': 'reviewer', 'password': 'reviewer_password', }) def test_view_cancelled_has_view_all_perm(self): create_user('reviewer', perms=['view_all_talks']) self.check_talk_view(self.talk_c, 200, auth={ 'username': 'reviewer', 'password': 'reviewer_password', }) def test_view_pending_has_view_all_perm(self): create_user('reviewer', perms=['view_all_talks']) self.check_talk_view(self.talk_p, 200, auth={ 'username': 'reviewer', 'password': 'reviewer_password', }) class TalkNoteViewTests(TestCase): def setUp(self): self.talk_a = create_talk("Talk A", ACCEPTED, "author_a") self.talk_r = create_talk("Talk R", REJECTED, "author_r") self.client = Client() def check_talk_view(self, talk, notes_visible, private_notes_visible, auth=None): if auth is not None: self.client.login(**auth) response = self.client.get( reverse('wafer_talk', kwargs={'pk': talk.pk})) if notes_visible: self.assertTrue('Some notes for talk' in response.rendered_content) else: # If the response doesn't have a rendered_content # (HttpResponseForbidden, etc), this is trivially true, # so we don't bother to test it. if hasattr(response, 'rendered_content'): self.assertFalse('Some notes for talk' in response.rendered_content) if private_notes_visible: self.assertTrue('Some private notes for talk' in response.rendered_content) else: if hasattr(response, 'rendered_content'): self.assertFalse('Some private notes for talk' in response.rendered_content) def test_view_notes_accepted_not_logged_in(self): self.check_talk_view(self.talk_a, False, False) def test_view_notes_accepted_author(self): self.check_talk_view(self.talk_a, False, False, auth={ 'username': 'author_a', 'password': 'author_a_password', }) def test_view_notes_rejected_author(self): self.check_talk_view(self.talk_r, False, False, auth={ 'username': 'author_r', 'password': 'author_r_password', }) def test_view_notes_accepted_has_view_all_perm(self): create_user('reviewer', perms=['view_all_talks']) self.check_talk_view(self.talk_a, True, False, auth={ 'username': 'reviewer', 'password': 'reviewer_password', }) def test_view_notes_rejected_has_view_all_perm(self): create_user('reviewer', perms=['view_all_talks']) self.check_talk_view(self.talk_r, True, False, auth={ 'username': 'reviewer', 'password': 'reviewer_password', }) def test_view_notes_accepted_has_edit_private_notes(self): create_user('editor', perms=['edit_private_notes']) self.check_talk_view(self.talk_a, False, True, auth={ 'username': 'editor', 'password': 'editor_password', }) def test_view_notes_rejected_has_edit_private_notes(self): # edit_private_notes doesn't imply view_all_talks create_user('editor', perms=['edit_private_notes']) self.check_talk_view(self.talk_r, False, False, auth={ 'username': 'editor', 'password': 'editor_password', }) def test_view_notes_rejected_both_perms(self): create_user('editor', perms=['edit_private_notes', 'view_all_talks']) self.check_talk_view(self.talk_r, True, True, auth={ 'username': 'editor', 'password': 'editor_password', }) def test_view_notes_accepted_superuser(self): create_user('super', superuser=True) self.check_talk_view(self.talk_a, True, True, auth={ 'username': 'super', 'password': 'super_password', }) def test_view_notes_rejected_superuser(self): create_user('super', superuser=True) self.check_talk_view(self.talk_r, True, True, auth={ 'username': 'super', 'password': 'super_password', }) class TalkUpdateTests(TestCase): def setUp(self): self.talk_a = create_talk("Talk A", ACCEPTED, "author_a") self.talk_r = create_talk("Talk R", REJECTED, "author_r") self.talk_p = create_talk("Talk P", PENDING, "author_p") self.client = Client() def check_talk_update(self, talk, status_code, auth=None): if auth is not None: self.client.login(**auth) response = self.client.get( reverse('wafer_talk_edit', kwargs={'pk': talk.pk})) self.assertEqual(response.status_code, status_code) return response def test_update_accepted_not_logged_in(self): self.check_talk_update(self.talk_a, 403) def test_update_rejected_not_logged_in(self): self.check_talk_update(self.talk_r, 403) def test_update_pending_not_logged_in(self): self.check_talk_update(self.talk_p, 403) def test_update_accepted_author(self): self.check_talk_update(self.talk_a, 403, auth={ 'username': 'author_a', 'password': 'author_a_password', }) def test_update_rejected_author(self): self.check_talk_update(self.talk_r, 403, auth={ 'username': 'author_r', 'password': 'author_r_password', }) def test_update_pending_author(self): self.check_talk_update(self.talk_p, 200, auth={ 'username': 'author_p', 'password': 'author_p_password', }) def test_update_accepted_superuser(self): create_user('super', superuser=True) self.check_talk_update(self.talk_a, 200, auth={ 'username': 'super', 'password': 'super_password', }) def test_update_rejected_superuser(self): create_user('super', superuser=True) self.check_talk_update(self.talk_r, 200, auth={ 'username': 'super', 'password': 'super_password', }) def test_update_pending_superuser(self): create_user('super', superuser=True) self.check_talk_update(self.talk_p, 200, auth={ 'username': 'super', 'password': 'super_password', }) def test_corresponding_author_displayed(self): response = self.check_talk_update(self.talk_p, 200, auth={ 'username': 'author_p', 'password': 'author_p_password', }) self.assertContains(response, ( '

    Submitted by author_p.

    '), html=True) class SpeakerTests(TestCase): def setUp(self): self.talk_a = create_talk("Talk A", ACCEPTED, "author_a") self.talk_r = create_talk("Talk R", REJECTED, "author_r") self.talk_p = create_talk("Talk P", PENDING, "author_p") self.client = Client() @mock.patch('wafer.users.models.UserProfile.avatar_url', mock_avatar_url) def test_view_one_speaker(self): img = self.talk_a.corresponding_author.userprofile.avatar_url() username = self.talk_a.corresponding_author.username response = self.client.get( reverse('wafer_talks_speakers')) self.assertEqual(response.status_code, 200) self.assertContains(response, "\n".join([ '', ]), html=True) def check_n_speakers(self, n, expected_rows): self.talk_a.delete() talks = [] for i in range(n): talks.append(create_talk("Talk %d" % i, ACCEPTED, "author_%d" % i)) profiles = [t.corresponding_author.userprofile for t in talks] response = self.client.get( reverse('wafer_talks_speakers')) self.assertEqual(response.status_code, 200) self.assertEqual(response.context["speaker_rows"], [ profiles[start:end] for start, end in expected_rows ]) @mock.patch('wafer.users.models.UserProfile.avatar_url', mock_avatar_url) def test_view_three_speakers(self): self.check_n_speakers(3, [(0, 3)]) @mock.patch('wafer.users.models.UserProfile.avatar_url', mock_avatar_url) def test_view_four_speakers(self): self.check_n_speakers(4, [(0, 4)]) @mock.patch('wafer.users.models.UserProfile.avatar_url', mock_avatar_url) def test_view_five_speakers(self): self.check_n_speakers(5, [(0, 4), (4, 5)]) @mock.patch('wafer.users.models.UserProfile.avatar_url', mock_avatar_url) def test_view_seven_speakers(self): self.check_n_speakers(7, [(0, 4), (4, 7)]) class TalkViewSetTests(TestCase): def setUp(self): self.talk_a = create_talk("Talk A", ACCEPTED, "author_a") self.talk_r = create_talk("Talk R", REJECTED, "author_r") self.talk_p = create_talk("Talk P", PENDING, "author_p") self.client = Client() def test_unauthorized_users(self): response = self.client.get('/talks/api/talks/') self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['results'][0]['title'], "Talk A") response = self.client.get('/talks/api/talks/%d/' % self.talk_a.talk_id) self.assertEqual(response.data['title'], 'Talk A') response = self.client.get('/talks/api/talks/%d/' % self.talk_r.talk_id) self.assertEqual(response.status_code, 404) def test_ordinary_users_get_accepted_talks(self): create_user('norm') self.client.login(username='norm', password='norm_password') response = self.client.get('/talks/api/talks/') self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['results'][0]['title'], "Talk A") response = self.client.get('/talks/api/talks/%d/' % self.talk_a.talk_id) self.assertEqual(response.data['title'], 'Talk A') response = self.client.get('/talks/api/talks/%d/' % self.talk_r.talk_id) self.assertEqual(response.status_code, 404) def test_super_user_gets_everything(self): create_user('super', True) self.client.login(username='super', password='super_password') response = self.client.get('/talks/api/talks/') self.assertEqual(response.data['count'], 3) self.assertEqual(response.data['results'][0]['title'], "Talk A") self.assertEqual(response.data['results'][1]['title'], "Talk R") self.assertEqual(response.data['results'][2]['title'], "Talk P") response = self.client.get('/talks/api/talks/%d/' % self.talk_a.talk_id) self.assertEqual(response.data['title'], 'Talk A') response = self.client.get('/talks/api/talks/%d/' % self.talk_r.talk_id) self.assertEqual(response.data['title'], 'Talk R') def test_reviewer_all_talks(self): create_user('reviewer', perms=['view_all_talks']) self.client.login(username='reviewer', password='reviewer_password') response = self.client.get('/talks/api/talks/') self.assertEqual(response.data['count'], 3) self.assertEqual(response.data['results'][0]['title'], "Talk A") self.assertEqual(response.data['results'][1]['title'], "Talk R") self.assertEqual(response.data['results'][2]['title'], "Talk P") response = self.client.get('/talks/api/talks/%d/' % self.talk_a.talk_id) self.assertEqual(response.data['title'], 'Talk A') response = self.client.get('/talks/api/talks/%d/' % self.talk_r.talk_id) self.assertEqual(response.data['title'], 'Talk R') def test_author_a_sees_own_talks_only(self): self.client.login(username='author_a', password='author_a_password') response = self.client.get('/talks/api/talks/') self.assertEqual(response.data['count'], 1) self.assertEqual(response.data['results'][0]['title'], "Talk A") def test_author_r_sees_own_talk(self): self.client.login(username='author_r', password='author_r_password') response = self.client.get('/talks/api/talks/') self.assertEqual(response.data['count'], 2) self.assertEqual(response.data['results'][0]['title'], "Talk A") self.assertEqual(response.data['results'][1]['title'], "Talk R") def test_author_p_sees_own_talk(self): self.client.login(username='author_p', password='author_p_password') response = self.client.get('/talks/api/talks/') self.assertEqual(response.data['count'], 2) self.assertEqual(response.data['results'][0]['title'], "Talk A") self.assertEqual(response.data['results'][1]['title'], "Talk P") PKyI1wafer/talks/migrations/0009_auto_20160813_1819.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('talks', '0008_auto_20160629_1404'), ] operations = [ migrations.AlterModelOptions( name='talktype', options={'ordering': ['order', 'id']}, ), migrations.AddField( model_name='talktype', name='disable_submission', field=models.BooleanField(default=False, help_text=b"Don't allow users to submit talks of this type."), ), migrations.AddField( model_name='talktype', name='order', field=models.IntegerField(default=1), ), ] PK'HIdm1wafer/talks/migrations/0003_talk_private_notes.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('talks', '0002_auto_20150813_2327'), ] operations = [ migrations.AddField( model_name='talk', name='private_notes', field=models.TextField(help_text='Note space for the conference organisers (not visible to submitter)', null=True, blank=True), preserve_default=True, ), ] PKOIGi  &wafer/talks/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings import markitup.fields class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Talk', fields=[ ('talk_id', models.AutoField(serialize=False, primary_key=True)), ('title', models.CharField(max_length=1024)), ('abstract', markitup.fields.MarkupField(help_text='Write two or three paragraphs describing your talk. Who is your audience? What will they get out of it? What will you cover?
    You can use Markdown syntax.', no_rendered_field=True)), ('notes', models.TextField(help_text='Any notes for the conference organisers?', null=True, blank=True)), ('status', models.CharField(default=b'P', max_length=1, choices=[(b'A', b'Accepted'), (b'R', b'Not Accepted'), (b'P', b'Under Consideration')])), ('_abstract_rendered', models.TextField(editable=False, blank=True)), ('authors', models.ManyToManyField(related_name='talks', to=settings.AUTH_USER_MODEL)), ('corresponding_author', models.ForeignKey(related_name='contact_talks', to=settings.AUTH_USER_MODEL)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='TalkType', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), ('description', models.TextField(max_length=1024)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='TalkUrl', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('description', models.CharField(max_length=256)), ('url', models.URLField()), ('talk', models.ForeignKey(to='talks.Talk')), ], options={ }, bases=(models.Model,), ), migrations.AddField( model_name='talk', name='talk_type', field=models.ForeignKey(to='talks.TalkType', null=True), preserve_default=True, ), ] PK}H `<wafer/talks/migrations/0004_edit_private_notes_permission.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('talks', '0003_talk_private_notes'), ] operations = [ migrations.AlterModelOptions( name='talk', options={'permissions': (('view_all_talks', 'Can see all talks'), ('edit_private_notes', 'Can edit the private notes fields'))}, ), ] PKH ~-1wafer/talks/migrations/0008_auto_20160629_1404.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('talks', '0007_add_ordering_option'), ] operations = [ migrations.AlterField( model_name='talk', name='status', field=models.CharField(default=b'P', max_length=1, choices=[(b'A', b'Accepted'), (b'R', b'Not Accepted'), (b'C', b'Talk Cancelled'), (b'P', b'Under Consideration')]), ), ] PKOIG"wafer/talks/migrations/__init__.pyPK(Hml2wafer/talks/migrations/0007_add_ordering_option.py# -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-04-07 10:51 from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('talks', '0006_author_helptext'), ] operations = [ migrations.AlterModelOptions( name='talktype', options={'ordering': ['id']}, ), ] PK}Hll˹%wafer/talks/migrations/0005_add_kv.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('kv', '__first__'), ('talks', '0004_edit_private_notes_permission'), ] operations = [ migrations.AddField( model_name='talk', name='kv', field=models.ManyToManyField(to='kv.KeyValue'), ), ] PK}H;::.wafer/talks/migrations/0006_author_helptext.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models from django.conf import settings class Migration(migrations.Migration): dependencies = [ ('talks', '0005_add_kv'), ] operations = [ migrations.AlterField( model_name='talk', name='authors', field=models.ManyToManyField(help_text='The speakers presenting the talk.', related_name='talks', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='talk', name='corresponding_author', field=models.ForeignKey(related_name='contact_talks', to=settings.AUTH_USER_MODEL, help_text='The person submitting the talk (and who questions regarding the talk should be addressed to).'), ), ] PK}HF1wafer/talks/migrations/0002_auto_20150813_2327.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('talks', '0001_initial'), ] operations = [ migrations.AlterModelOptions( name='talk', options={'permissions': (('view_all_talks', 'Can see all talks'),)}, ), ] PKOIGwafer/tests/test_menu.pyPKOIGwafer/tests/__init__.pyPK(H6..wafer/pages/urls.pyfrom django.conf.urls import patterns, url, include from django.core.urlresolvers import get_script_prefix from django.views.generic import RedirectView from rest_framework import routers from wafer.pages.views import PageViewSet router = routers.DefaultRouter() router.register(r'pages', PageViewSet) urlpatterns = patterns( 'wafer.pages.views', url(r'^api/', include(router.urls)), url('^index(?:\.html)?/?$', RedirectView.as_view( url=get_script_prefix(), query_string=True)), url(r'^(?:(.+)/)?$', 'slug', name='wafer_page'), ) PKyI/.wafer/pages/views.pyfrom django.http import Http404 from django.core.exceptions import PermissionDenied from django.views.generic import DetailView, TemplateView, UpdateView from reversion import revisions from reversion.models import Version from rest_framework import viewsets from rest_framework.permissions import DjangoModelPermissionsOrAnonReadOnly from wafer.compare.admin import make_diff, get_author, get_date from wafer.pages.models import Page from wafer.pages.serializers import PageSerializer from wafer.pages.forms import PageForm class ShowPage(DetailView): template_name = 'wafer.pages/page.html' model = Page class EditPage(UpdateView): template_name = 'wafer.pages/page_form.html' model = Page form_class = PageForm @revisions.create_revision() def form_valid(self, form): revisions.set_user(self.request.user) revisions.set_comment("Page Modified") return super(EditPage, self).form_valid(form) class ComparePage(DetailView): template_name = 'wafer.pages/page_compare.html' model = Page def get_context_data(self, **kwargs): context = super(ComparePage, self).get_context_data(**kwargs) versions = Version.objects.get_for_object(self.object) # By revisions api definition, this is the most recent version current = versions[0] context['cur_author'] = get_author(versions[0]) context['cur_date'] = get_date(versions[0]) context['prev_author'] = None context['prev_date'] = None context['prev'] = None context['next'] = None context['diff_list'] = None if len(versions) == 1: # only 1 version, so short circuit everything return context requested_version = int(self.request.GET.get('version', 1)) if requested_version > len(versions): # Incorrect data, so fail to sane state return context if requested_version == 1: # No next revision in this case context['next'] = None else: context['next'] = requested_version - 1 if requested_version >= len(versions) - 1: # No previous revision context['prev'] = None else: context['prev'] = requested_version + 1 previous = versions[requested_version] context['prev_author'] = get_author(previous) context['prev_date'] = get_date(previous) context['diff_list'] = make_diff(current, previous) return context def slug(request, url): """Look up a page by url (which is a tree of slugs)""" page = None if url: for slug in url.split('/'): if not slug: continue try: page = Page.objects.get(slug=slug, parent=page) except Page.DoesNotExist: raise Http404 else: try: page = Page.objects.get(slug='index') except Page.DoesNotExist: return TemplateView.as_view( template_name='wafer/index.html')(request) if 'edit' in request.GET: if not request.user.has_perm('pages.change_page'): raise PermissionDenied return EditPage.as_view()(request, pk=page.id) if 'compare' in request.GET: if not request.user.has_perm('pages.change_page'): raise PermissionDenied return ComparePage.as_view()(request, pk=page.id) return ShowPage.as_view()(request, pk=page.id) class PageViewSet(viewsets.ModelViewSet): """API endpoint for users.""" queryset = Page.objects.all() serializer_class = PageSerializer permission_classes = (DjangoModelPermissionsOrAnonReadOnly, ) PKOIG2R44wafer/pages/forms.pyfrom django.forms import ModelForm from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit from markitup.widgets import MarkItUpWidget from wafer.pages.models import Page class PageForm(ModelForm): def __init__(self, *args, **kwargs): super(PageForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.add_input(Submit('submit', 'Submit')) class Meta: model = Page fields = ['name', 'content'] widgets = { 'content': MarkItUpWidget(), } PK}H͹_lwafer/pages/models.pyimport logging logger = logging.getLogger(__name__) from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse from django.core.exceptions import ValidationError, NON_FIELD_ERRORS from django.conf import settings from django.db import models from django.db.models.signals import post_save from django.utils.encoding import python_2_unicode_compatible from markitup.fields import MarkupField from wafer.menu import MenuError, refresh_menu_cache @python_2_unicode_compatible class File(models.Model): """A file for use in page markup.""" name = models.CharField(max_length=255) description = models.TextField() item = models.FileField(upload_to='pages_files') def __str__(self): return u'%s' % (self.name,) @python_2_unicode_compatible class Page(models.Model): """An extra page for the site.""" name = models.CharField(max_length=255) slug = models.SlugField(help_text=_("Last component of the page URL")) parent = models.ForeignKey('self', null=True, blank=True) content = MarkupField( help_text=_("Markdown contents for the page.")) include_in_menu = models.BooleanField( help_text=_("Whether to include in menus."), default=False) exclude_from_static = models.BooleanField( help_text=_("Whether to exclude this page from the static version of" " the site (Container pages, etc.)"), default=False) files = models.ManyToManyField( File, related_name="pages", blank=True, help_text=_("Images and other files for use in" " the content markdown field.")) people = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name='pages', blank=True, help_text=_("People associated with this page for display in the" " schedule (Session chairs, panelists, etc.)")) def __str__(self): return u'%s' % (self.name,) def get_path(self): path, parent = [self.slug], self.parent while parent is not None: path.insert(0, parent.slug) parent = parent.parent return path def get_absolute_url(self): url = "/".join(self.get_path()) return reverse('wafer_page', args=(url,)) get_absolute_url.short_description = 'page url' def get_in_schedule(self): if self.scheduleitem_set.all(): return True return False def get_people_display_names(self): names = [person.userprofile.display_name() for person in self.people.all()] if len(names) > 2: comma_names = ', '.join(names[:-1]) return comma_names + ' and ' + names[-1] else: return ' and '.join(names) get_in_schedule.short_description = 'Added to schedule' get_in_schedule.boolean = True get_people_display_names.short_description = 'People' class Model: unique_together = (('parent', 'slug'),) def clean(self): keys = [self.pk] parent = self.parent while parent is not None: if parent.pk in keys: raise ValidationError( { NON_FIELD_ERRORS: [ _("Circular reference in parent."), ], }) keys.append(parent.pk) parent = parent.parent return super(Page, self).clean() def validate_unique(self, exclude=None): existing = Page.objects.filter(slug=self.slug, parent=self.parent) # We could be updating the page, so don't fail if the existing # entry is this page. if existing.count() > 1 or (existing.count() == 1 and existing.first().pk != self.pk): raise ValidationError( { NON_FIELD_ERRORS: [ _("Duplicate parent/slug combination."), ], }) return super(Page, self).validate_unique(exclude) def page_menus(root_menu): """Add page menus.""" for page in Page.objects.filter(include_in_menu=True): path = page.get_path() menu = path[0] if len(path) > 1 else None try: root_menu.add_item(page.name, page.get_absolute_url(), menu=menu) except MenuError as e: logger.error("Bad menu item %r for page with slug %r." % (e, page.slug)) post_save.connect(refresh_menu_cache, sender=Page) PKOIGhMMT``wafer/pages/renderers.pyfrom django_medusa.renderers import StaticSiteRenderer from wafer.pages.models import Page class PagesRenderer(StaticSiteRenderer): def get_paths(self): paths = [] items = Page.objects.all() for item in items: if item.exclude_from_static: # Container page continue url = item.get_absolute_url() # FIXME: Can we introspect this easily from urls? if url == '/index' or url == '/index.html': url = '/' paths.append(url) return paths renderers = [PagesRenderer, ] PKOIGwafer/pages/__init__.pyPK}H rwafer/pages/admin.pyfrom django.contrib import admin from wafer.pages.models import File, Page from wafer.compare.admin import CompareVersionAdmin, DateModifiedFilter class PageAdmin(CompareVersionAdmin, admin.ModelAdmin): prepopulated_fields = {"slug": ("name",)} list_display = ('name', 'slug', 'get_absolute_url', 'get_people_display_names', 'get_in_schedule') list_filter = (DateModifiedFilter,) admin.site.register(Page, PageAdmin) admin.site.register(File) PK}H2 00wafer/pages/serializers.pyfrom django.contrib.auth import get_user_model from rest_framework import serializers from reversion import revisions from wafer.pages.models import Page class PageSerializer(serializers.ModelSerializer): people = serializers.PrimaryKeyRelatedField( many=True, allow_null=True, queryset=get_user_model().objects.all()) class Meta: model = Page exclude = ('_content_rendered',) @revisions.create_revision() def create(self, validated_data): revisions.set_comment("Created via REST api") return super(PageSerializer, self).create(validated_data) @revisions.create_revision() def update(self, page, validated_data): revisions.set_comment("Changed via REST api") page.parent = validated_data['parent'] page.content = validated_data['content'] page.include_in_menu = validated_data['include_in_menu'] page.exclude_from_static = validated_data['exclude_from_static'] page.people = validated_data.get('people') page.save() return page PKyIoL+wafer/pages/templates/wafer.pages/page.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}
    {{ page.content.rendered|safe }} {% if perms.pages.change_page %} {% trans 'Compare to Previous version' %} {% trans 'Edit' %} {% endif %}
    {% endblock %} PKyIo3wafer/pages/templates/wafer.pages/page_compare.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    Comparing Page

    Last Saved: {{ cur_date }} by {{ cur_author }}

    {% if prev_date %}

    Comparing to version saved: {{ prev_date }} by {{ prev_author }}

    {% for field, diff in diff_list %}

    {{ field }}

    {{ diff | safe }}
    {% empty %}

    {% trans 'No differences found' %}

    {% endfor %}
    {# Add navigation buttons #} {% else %}

    No previous versions to compare to

    {% endif %} {% endblock %} PKOIGV0wafer/pages/templates/wafer.pages/page_form.html{% extends 'wafer/base.html' %} {% load crispy_forms_tags %} {% load i18n %} {% block content %}

    {% trans "Edit Page" %}

    {{ form.media }} {% crispy form %}
    {% endblock %} PK(HMwafer/pages/tests/test_pages.py# Simple test of the edit logic around pages from django.test import Client, TestCase from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from wafer.pages.models import Page class PageEditTests(TestCase): def _make_users(self): # utiltiy function for tests UserModel = get_user_model() # Make user without edit permissions try: no_edit_user = UserModel.objects.filter(username='no_edit').get() except UserModel.DoesNotExist: no_edit_user = UserModel.objects.create_user('no_edit', 'test@test', 'aaaa') no_edit_user.save() # Make user with edit permissions try: edit_user = UserModel.objects.filter(username='can_edit').get() except UserModel.DoesNotExist: edit_user = UserModel.objects.create_user('can_edit', 'test@test', 'aaaa') # Is there a better way to do this? page_content_type = ContentType.objects.get_for_model(Page) change_page = Permission.objects.get( content_type=page_content_type, codename='change_page') edit_user.user_permissions.add(change_page) edit_user.save() return no_edit_user, edit_user def test_simple_page(self): # Test editing a page we create no_edit_user, edit_user = self._make_users() page = Page.objects.create(name="test edit page", slug="test_edit") page.save() c = Client() # Test without edit permission c.login(username=no_edit_user.username, password='aaaa') response = c.get('/test_edit/') templates = [x.name for x in response.templates] self.assertTrue('wafer.pages/page.html' in templates) response = c.get('/test_edit/', {'edit': ''}) self.assertEqual(response.status_code, 403) c.logout() # Test with edit permission c.login(username=edit_user.username, password='aaaa') response = c.get('/test_edit/') templates = [x.name for x in response.templates] self.assertTrue('wafer.pages/page.html' in templates) response = c.get('/test_edit/', {'edit': ''}) templates = [x.name for x in response.templates] self.assertTrue('wafer.pages/page_form.html' in templates) self.assertEqual(response.status_code, 200) def test_root_page(self): # Test editing / no_edit_user, edit_user = self._make_users() page = Page.objects.create(name="index", slug="index") page.save() c = Client() # Test without edit permission c.login(username=no_edit_user.username, password='aaaa') response = c.get('/index', follow=True) templates = [x.name for x in response.templates] self.assertTrue('wafer.pages/page.html' in templates) response = c.get('/index', {'edit': ''}, follow=True) templates = [x.name for x in response.templates] self.assertEqual(response.status_code, 403) c.logout() # Test with edit permission c.login(username=edit_user.username, password='aaaa') response = c.get('/index', follow=True) templates = [x.name for x in response.templates] self.assertTrue('wafer.pages/page.html' in templates) response = c.get('/index', {'edit': ''}, follow=True) templates = [x.name for x in response.templates] self.assertTrue('wafer.pages/page_form.html' in templates) self.assertEqual(response.status_code, 200) PKKH) UU4wafer/pages/migrations/0003_non-null_people+files.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models from django.conf import settings class Migration(migrations.Migration): dependencies = [ ('pages', '0002_page_people'), ] operations = [ migrations.AlterField( model_name='page', name='files', field=models.ManyToManyField(help_text='Images and other files for use in the content markdown field.', related_name='pages', to='pages.File', blank=True), ), migrations.AlterField( model_name='page', name='people', field=models.ManyToManyField(help_text='People associated with this page for display in the schedule (Session chairs, panelists, etc.)', related_name='pages', to=settings.AUTH_USER_MODEL, blank=True), ), ] PKOIG/yQ~~&wafer/pages/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import markitup.fields class Migration(migrations.Migration): dependencies = [ ] operations = [ migrations.CreateModel( name='File', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), ('description', models.TextField()), ('item', models.FileField(upload_to=b'pages_files')), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Page', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=255)), ('slug', models.SlugField(help_text='Last component of the page URL')), ('content', markitup.fields.MarkupField(help_text='Markdown contents for the page.', no_rendered_field=True)), ('include_in_menu', models.BooleanField(default=False, help_text='Whether to include in menus.')), ('exclude_from_static', models.BooleanField(default=False, help_text='Whether to exclude this page from the static version of the site (Container pages, etc.)')), ('_content_rendered', models.TextField(editable=False, blank=True)), ('files', models.ManyToManyField(help_text='Images and other files for use in the content markdown field.', related_name='pages', null=True, to='pages.File', blank=True)), ('parent', models.ForeignKey(blank=True, to='pages.Page', null=True)), ], options={ }, bases=(models.Model,), ), ] PKOIG"wafer/pages/migrations/__init__.pyPKOIGq*wafer/pages/migrations/0002_page_people.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('pages', '0001_initial'), ] operations = [ migrations.AddField( model_name='page', name='people', field=models.ManyToManyField(help_text='People associated with this page for display in the schedule (Session chairs, panelists, etc.)', related_name='pages', null=True, to=settings.AUTH_USER_MODEL, blank=True), preserve_default=True, ), ] PK}Hwafer/users/urls.pyfrom django.conf.urls import patterns, url, include from rest_framework import routers from wafer.users.views import (UsersView, ProfileView, EditProfileView, EditUserView, RegistrationView, UserViewSet) router = routers.DefaultRouter() router.register(r'users', UserViewSet) urlpatterns = patterns( '', url(r'^$', UsersView.as_view(), name='wafer_users'), url(r'^api/', include(router.urls)), url(r'^page/(?P\d+)$', UsersView.as_view(), name='wafer_users_page'), url(r'^(?P[\w.@+-]+)/$', ProfileView.as_view(), name='wafer_user_profile'), url(r'^(?P[\w.@+-]+)/edit/$', EditUserView.as_view(), name='wafer_user_edit'), url(r'^(?P[\w.@+-]+)/edit_profile/$', EditProfileView.as_view(), name='wafer_user_edit_profile'), url(r'^(?P[\w.@+-]+)/register/$', RegistrationView.as_view(), name='wafer_register_view'), ) PK(H69"9"wafer/users/views.pyimport logging from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.contrib.sites.shortcuts import get_current_site from django.core.exceptions import ( ObjectDoesNotExist, PermissionDenied, ValidationError, ) from django.core.mail import EmailMultiAlternatives from django.core.urlresolvers import reverse from django.http import Http404 from django.shortcuts import render from django.template import RequestContext, TemplateDoesNotExist from django.template.loader import render_to_string from django.utils.translation import ugettext as _ from django.views.generic import DetailView, UpdateView from django.views.generic.edit import FormView from django.views.generic.list import ListView from rest_framework import viewsets from rest_framework.permissions import IsAdminUser from wafer.kv.utils import deserialize_by_field from wafer.users.forms import ( UserForm, UserProfileForm, get_registration_form_class, ) from wafer.users.serializers import UserSerializer from wafer.users.models import UserProfile log = logging.getLogger(__name__) class UsersView(ListView): template_name = 'wafer.users/users.html' model = get_user_model() paginate_by = 25 def get_queryset(self, *args, **kwargs): if not settings.WAFER_PUBLIC_ATTENDEE_LIST: raise Http404() return super(UsersView, self).get_queryset(*args, **kwargs) class ProfileView(DetailView): template_name = 'wafer.users/profile.html' model = get_user_model() slug_field = 'username' slug_url_kwarg = 'username' # avoid a clash with the user object used by the menus context_object_name = 'profile_user' def get_object(self, *args, **kwargs): object_ = super(ProfileView, self).get_object(*args, **kwargs) if not settings.WAFER_PUBLIC_ATTENDEE_LIST: if (not self.can_edit(object_) and not object_.userprofile.accepted_talks().exists()): raise Http404() return object_ def get_context_data(self, **kwargs): context = super(ProfileView, self).get_context_data(**kwargs) context['can_edit'] = self.can_edit(context['object']) return context def can_edit(self, user): is_self = user == self.request.user return is_self or self.request.user.has_perm( 'users.change_userprofile') # TODO: Combine these class EditOneselfMixin(object): def get_object(self, *args, **kwargs): object_ = super(EditOneselfMixin, self).get_object(*args, **kwargs) self.verify_edit_permission(object_) return object_ def verify_edit_permission(self, object_): if hasattr(object_, 'user'): # Accept User or UserProfile object_ = object_.user if object_ == self.request.user or self.request.user.has_perm( 'users.change_userprofile'): return if settings.WAFER_PUBLIC_ATTENDEE_LIST: raise Http404() else: raise PermissionDenied() class EditUserView(EditOneselfMixin, UpdateView): template_name = 'wafer.users/edit_user.html' slug_field = 'username' slug_url_kwarg = 'username' model = get_user_model() form_class = UserForm # avoid a clash with the user object used by the menus context_object_name = 'profile_user' def get_success_url(self): return reverse('wafer_user_profile', args=(self.object.username,)) class EditProfileView(EditOneselfMixin, UpdateView): template_name = 'wafer.users/edit_profile.html' slug_field = 'user__username' slug_url_kwarg = 'username' model = UserProfile form_class = UserProfileForm # avoid a clash with the user object used by the menus context_object_name = 'profile_user' def get_success_url(self): return reverse('wafer_user_profile', args=(self.object.user.username,)) class RegistrationView(EditOneselfMixin, FormView): template_name = 'wafer.users/registration/form.html' success_template_name = 'wafer.users/registration/success.html' confirm_mail_txt_template_name = ( 'wafer.users/registration/confirm_mail.txt') confirm_mail_html_template_name = ( 'wafer.users/registration/confirm_mail.html') def get_user(self): try: return UserProfile.objects.get( user__username=self.kwargs['username']) except UserProfile.DoesNotExist: raise Http404() def get_form_class(self): return get_registration_form_class() def get_kv_group(self): return Group.objects.get_by_natural_key('Registration') def get_queryset(self): if settings.WAFER_REGISTRATION_MODE != 'form': raise Http404('Form-based registration is not in use') user = self.get_user() self.verify_edit_permission(user) return user.kv.filter(group=self.get_kv_group()) def get_initial(self): saved = self.get_queryset() form = self.get_form_class()() initial = form.initial_values(self.get_user()) for fieldname in form.fields: try: value = saved.get(key=fieldname).value field = form.fields[fieldname] initial[fieldname] = deserialize_by_field(value, field) except ObjectDoesNotExist: continue return initial def form_invalid(self, form): log.info('User %s posted an incomplete registration form', self.get_user().user.username) return super(RegistrationView, self).form_invalid(form) def form_valid(self, form): if not settings.WAFER_REGISTRATION_OPEN: raise ValidationError(_('Registration is not open')) saved = self.get_queryset() user = self.get_user() group = self.get_kv_group() for key, value in form.cleaned_data.items(): try: pair = saved.get(key=key) pair.value = value pair.save() except ObjectDoesNotExist: user.kv.create(group=group, key=key, value=value) log.info('User %s successfully registered (%r)', user.user.username, form.cleaned_data) is_registered = form.is_registered(self.get_queryset()) send_email = (getattr(form, 'send_email_confirmation', False) and is_registered) confirmation_context = self.get_confirmation_context_data( form, send_email, is_registered) context_instance = RequestContext(self.request, confirmation_context) if send_email: self.email_confirmation(context_instance) return self.confirmation_response(context_instance) def get_confirmation_context_data(self, form, will_send_email, is_registered): registration_data = [] for fieldname, field in form.fields.items(): registration_data.append({ 'name': fieldname, 'label': field.label, 'value': form.cleaned_data.get(fieldname), }) context = self.get_context_data() context['form'] = form context['registered'] = is_registered context['registration_data'] = registration_data context['will_send_email'] = will_send_email context['talks_open'] = settings.WAFER_TALKS_OPEN return context def email_confirmation(self, context_instance): conference_name = get_current_site(self.request).name subject = _('%s Registration Confirmation') % conference_name txt = render_to_string(self.confirm_mail_txt_template_name, context_instance=context_instance) try: html = render_to_string(self.confirm_mail_html_template_name, context_instance=context_instance) except TemplateDoesNotExist: html = None to = self.get_user().user.email email_message = EmailMultiAlternatives(subject, txt, to=[to]) if html: email_message.attach_alternative(html, "text/html") email_message.send() def confirmation_response(self, context_instance): return render(self.request, self.success_template_name, context_instance=context_instance) class UserViewSet(viewsets.ModelViewSet): """API endpoint for users.""" queryset = get_user_model().objects.all() serializer_class = UserSerializer # We want some better permissions than the default here, but # IsAdminUser will do for now. permission_classes = (IsAdminUser, ) PK}HGژ wafer/users/forms.pyfrom django import forms from django.conf import settings from django.contrib.auth import get_user_model from django.core.urlresolvers import reverse from django.forms import fields from django.utils.module_loading import import_string from django.utils.translation import ugettext as _ from crispy_forms.bootstrap import PrependedText from crispy_forms.helper import FormHelper from crispy_forms.layout import Fieldset, Layout, Submit from wafer.users.models import UserProfile class UserForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(UserForm, self).__init__(*args, **kwargs) self.helper = FormHelper() username = kwargs['instance'].username self.helper.form_action = reverse('wafer_user_edit', args=(username,)) self.helper.add_input(Submit('submit', _('Save'))) self.fields['first_name'].required = True self.fields['email'].required = True class Meta: # TODO: Password reset model = get_user_model() fields = ('username', 'first_name', 'last_name', 'email') class UserProfileForm(forms.ModelForm): def __init__(self, *args, **kwargs): super(UserProfileForm, self).__init__(*args, **kwargs) self.helper = FormHelper(self) username = kwargs['instance'].user.username self.helper.form_action = reverse('wafer_user_edit_profile', args=(username,)) self.helper['twitter_handle'].wrap(PrependedText, 'twitter_handle', '@', placeholder=_('handle')) self.helper['github_username'].wrap(PrependedText, 'github_username', '@', placeholder=_('username')) self.helper.add_input(Submit('submit', _('Save'))) class Meta: model = UserProfile exclude = ('user', 'kv') def get_registration_form_class(): return import_string(settings.WAFER_REGISTRATION_FORM) class ExampleRegistrationForm(forms.Form): debcamp = fields.BooleanField( label=_('Plan to attend DebCamp'), required=False) debconf = fields.BooleanField( label=_('Plan to attend DebConf'), required=False) require_sponsorship = fields.BooleanField( label=_('Will require sponsorship'), required=False) def __init__(self, *args, **kwargs): super(ExampleRegistrationForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.layout = Layout( Fieldset(_('Pre-Registration'), 'debcamp', 'debconf', 'require_sponsorship')) self.helper.add_input(Submit('submit', _('Save'))) @classmethod def is_registered(cls, kv_data): """ Given a user's kv_data query, determine if they have registered to attend. """ for item in kv_data.filter(key__in=('debcamp', 'debconf')): if item.value is True: return True return False def initial_values(self, user): """Set default values, based on the user""" return {'debconf': True} PK(Hý wafer/users/models.pyfrom django.conf import settings from django.contrib.auth.models import User from django.db import models from django.db.models.signals import post_save from django.utils.encoding import python_2_unicode_compatible from libravatar import libravatar_url try: from urllib2 import urlparse except ImportError: from urllib import parse as urlparse from wafer.kv.models import KeyValue from wafer.talks.models import ACCEPTED, PENDING @python_2_unicode_compatible class UserProfile(models.Model): class Meta: ordering = ['id'] user = models.OneToOneField(User) kv = models.ManyToManyField(KeyValue) contact_number = models.CharField(max_length=16, null=True, blank=True) bio = models.TextField(null=True, blank=True) homepage = models.CharField(max_length=256, null=True, blank=True) # We should probably do social auth instead # And care about other code hosting sites... twitter_handle = models.CharField(max_length=15, null=True, blank=True) github_username = models.CharField(max_length=32, null=True, blank=True) def __str__(self): return u'%s' % self.user def accepted_talks(self): return self.user.talks.filter(status=ACCEPTED) def pending_talks(self): return self.user.talks.filter(status=PENDING) def avatar_url(self, size=96, https=True, default='mm'): if not self.user.email: return None return libravatar_url(self.user.email, size=size, https=https, default=default) def homepage_url(self): """Try ensure we prepend http: to the url if there's nothing there This is to ensure we're not generating relative links in the user templates.""" if not self.homepage: return self.homepage parsed = urlparse.urlparse(self.homepage) if parsed.scheme: return self.homepage # Vague sanity check abs_url = ''.join(['http://', self.homepage]) if urlparse.urlparse(abs_url).scheme == 'http': return abs_url return self.homepage def display_name(self): return self.user.get_full_name() or self.user.username def is_registered(self): from wafer.users.forms import get_registration_form_class if settings.WAFER_REGISTRATION_MODE == 'ticket': return self.user.ticket.exists() elif settings.WAFER_REGISTRATION_MODE == 'form': form = get_registration_form_class() return form.is_registered(self.kv) raise NotImplemented('Invalid WAFER_REGISTRATION_MODE: %s' % settings.WAFER_REGISTRATION_MODE) is_registered.boolean = True def create_user_profile(sender, instance, created, raw=False, **kwargs): if raw: return if created: UserProfile.objects.create(user=instance) post_save.connect(create_user_profile, sender=User) PK(Hx,,wafer/users/renderers.pyfrom django_medusa.renderers import StaticSiteRenderer from django.core.urlresolvers import reverse from django.contrib.auth import get_user_model from django.conf import settings from wafer.users.views import UsersView class UserRenderer(StaticSiteRenderer): def get_paths(self): paths = [] if settings.WAFER_PUBLIC_ATTENDEE_LIST: paths.append("/users/") items = get_user_model().objects.all() for item in items: if not settings.WAFER_PUBLIC_ATTENDEE_LIST: # We need to filter out the non-publically # accessible paths from the static site if not item.userprofile.accepted_talks().exists(): continue paths.append(reverse('wafer_user_profile', kwargs={'username': item.username})) if settings.WAFER_PUBLIC_ATTENDEE_LIST: view = UsersView() queryset = view.get_queryset() paginator = view.get_paginator(queryset, view.get_paginate_by(queryset)) for page in paginator.page_range: paths.append(reverse('wafer_users_page', kwargs={'page': page})) return paths renderers = [UserRenderer, ] PKOIGwafer/users/__init__.pyPKH9XOOwafer/users/admin.pyfrom django.contrib import admin from django.contrib.auth.admin import UserAdmin from django.contrib.auth import get_user_model from wafer.users.models import UserProfile # User-centric class UserProfileInline(admin.StackedInline): model = UserProfile can_delete = False verbose_name_plural = 'profile' exclude = ('kv',) class UserAdmin(UserAdmin): inlines = (UserProfileInline,) def username(obj): return obj.user.username def email(obj): return obj.user.email admin.site.unregister(get_user_model()) admin.site.register(get_user_model(), UserAdmin) PK}H{wafer/users/serializers.pyfrom rest_framework import serializers from django.contrib.auth import get_user_model class UserSerializer(serializers.ModelSerializer): class Meta: model = get_user_model() # Arguably not useful to expose via the REST api without # more thought. # is_superuser seems dangerous to allow through the REST api exclude = ('password', 'is_superuser') PKyIۙ.wafer/users/templates/wafer.users/profile.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %} {% with profile=object.userprofile %}
    {% with profile.avatar_url as avatar_url %} {% if avatar_url != None %} {% endif %} {% endwith %} {% if can_edit %} {% trans 'Edit Mugshot' %}
    {% blocktrans %} Pictures provided by libravatar (which falls back to Gravatar).
    Change your picture there. {% endblocktrans %}
    {% endif %}
    {% if can_edit %} {% endif %} {% spaceless %}

    {% if profile.homepage %} {% endif %} {{ profile.display_name }} {% if profile.homepage %} {% endif %}

    {% if profile.twitter_handle %}

    {% endif %} {% if profile.github_username %}

    {% blocktrans with username=profile.github_username %}GitHub: {{ username }}{% endblocktrans %}

    {% endif %} {% endspaceless %}
    {% if profile.bio %}
    {{ profile.bio|linebreaks }}
    {% endif %} {% if can_edit %} {% if profile.pending_talks.exists or profile.accepted_talks.exists %} {% if profile.is_registered %}
    {% blocktrans %} Registered {% endblocktrans %}
    {% else %}
    {% blocktrans %} WARNING: Talk proposal submitted, but not registered. {% endblocktrans %} {% if WAFER_REGISTRATION_OPEN %} {% trans "Register now!" %} {% endif %}
    {% endif %} {% endif %} {% endif %} {# Accepted talks are globally visible #} {% if profile.accepted_talks.exists %}

    {% trans 'Accepted Talks:' %}

    {% for talk in profile.accepted_talks %}
    {{ talk.title }}

    {{ talk.abstract.rendered|safe }}

    {% endfor %} {% endif %} {# Submitted talk proposals are only visible to the owner #} {% if can_edit %} {% if profile.pending_talks.exists %}

    {% trans 'Submitted Talks (under consideration):' %}

    {% for talk in profile.pending_talks %}
    {{ talk.title }} {% comment %} Because this is one of the author's pending talks, we don't need to check for edit permission's on the talk explictly. This doesn't show the edit button for people with 'change-talk' permissions, but we accept that tradeoff for simplicity here. {% endcomment %} {% trans 'Edit' %}

    {{ talk.abstract.rendered|safe }}

    {% endfor %} {% endif %} {% endif %} {% endwith %} {% endblock %} {% block extra_foot %} {% endblock %} PK(H1LpG0wafer/users/templates/wafer.users/edit_user.html{% extends "wafer/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block content %}

    {% trans 'Edit User:' %}

    {% if profile_user == user and profile_user.has_usable_password %} {% endif %} {% crispy form %} {% endblock %} PKcGW,wafer/users/templates/wafer.users/users.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {% trans 'Users:' %}

    {% for user in user_list %}
    {{ user.userprofile.display_name }}
    {% endfor %} {% if is_paginated %}
      {% if page_obj.has_previous %}
    • «
    • {% else %}
    • «
    • {% endif %} {% for page in paginator.page_range %}
    • {{ page }}
    • {% endfor %} {% if page_obj.has_next %}
    • »
    • {% else %}
    • »
    • {% endif %}
    {% endif %} {% endblock %} PKOIG3wafer/users/templates/wafer.users/edit_profile.html{% extends "wafer/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block content %}

    {% trans 'Edit Profile:' %}

    {% crispy form %} {% endblock %} PK}H9*???wafer/users/templates/wafer.users/registration/confirm_mail.txt{% load i18n %} {% blocktrans %} Thank you for registering for {{ WAFER_CONFERENCE_NAME }}. This e-mail confirms that we have your registration. You can come back and edit your registration, at any time (until registration closes). Regards, The {{ WAFER_CONFERENCE_NAME }} conference organisers. {% endblocktrans %} PK}H\8wafer/users/templates/wafer.users/registration/form.html{% extends "wafer/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block content %}

    {% trans 'Registration:' %}

    {% if not WAFER_REGISTRATION_OPEN %} {% trans "Registration is not currently open" %} {% else %} {% crispy form %} {% endif %} {% endblock %} PK(H7';wafer/users/templates/wafer.users/registration/success.html{% extends "wafer/base.html" %} {% load i18n %} {% load crispy_forms_tags %} {% block content %} {% if not registered %}

    {% blocktrans %} You are not registered for {{ WAFER_CONFERENCE_NAME }} {% endblocktrans %}

    {% blocktrans %} You can come back and register, at any time (until registration closes). {% endblocktrans %}

    {% else %}

    {% trans 'You have been registered' %}

    {% blocktrans %} Thank you for registering for {{ WAFER_CONFERENCE_NAME }}. {% endblocktrans %} {% if will_send_email %} {% trans 'You should receive a confirmation e-mail, shortly.' %} {% endif %} {% blocktrans %} You can come back and edit your registration, at any time (until registration closes). {% endblocktrans %}

    {% if talks_open %}

    {% url 'wafer_talk_submit' as submit_url %} {% blocktrans %} Now would be a great time to Submit a Talk Proposal. {% endblocktrans %}

    {% endif %} {% endif %}

    {% trans 'Back to my profile' %}

    {% endblock %} PK}H[ą wafer/users/tests/test_models.py# -*- coding: utf-8 -*- # vim:fileencoding=utf-8 ai ts=4 sts=4 et sw=4 """Tests for wafer.user.models""" from django.contrib.auth import get_user_model from django.test import TestCase import sys PY2 = sys.version_info[0] == 2 class UserModelTestCase(TestCase): def test_str_method_issue192(self): """Test that str(user) works correctly""" create = get_user_model().objects.create_user user = create('test', 'test@example.com', 'test_pass') self.assertEqual(str(user.userprofile), 'test') user = create(u'tést', 'test@example.com', 'test_pass') if PY2: self.assertEqual(unicode(user.userprofile), u'tést') else: self.assertEqual(str(user.userprofile), u'tést') PKOIG@RR&wafer/users/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations from django.conf import settings class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='UserProfile', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('contact_number', models.CharField(max_length=16, null=True, blank=True)), ('bio', models.TextField(null=True, blank=True)), ('homepage', models.CharField(max_length=256, null=True, blank=True)), ('twitter_handle', models.CharField(max_length=15, null=True, blank=True)), ('github_username', models.CharField(max_length=32, null=True, blank=True)), ('user', models.OneToOneField(to=settings.AUTH_USER_MODEL)), ], options={ }, bases=(models.Model,), ), ] PK}HHڪ-wafer/users/migrations/0002_userprofile_kv.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('kv', '__first__'), ('users', '0001_initial'), ] operations = [ migrations.AddField( model_name='userprofile', name='kv', field=models.ManyToManyField(to='kv.KeyValue'), ), ] PKOIG"wafer/users/migrations/__init__.pyPK(H(=D2nn1wafer/users/migrations/0003_auto_20160329_2003.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('users', '0002_userprofile_kv'), ] operations = [ migrations.AlterModelOptions( name='userprofile', options={'ordering': ['id']}, ), ] PKOIGwafer/snippets/__init__.pyPK}HM wafer/snippets/markdown_field.py# From http://djangosnippets.org/snippets/882/ # Copyright carljm (http://djangosnippets.org/users/carljm/) from django.db.models import TextField from markdown import markdown class MarkdownTextField(TextField): """ A TextField that automatically implements DB-cached Markdown translation. Accepts two additional keyword arguments: if allow_html is False, Markdown will be called in safe mode, which strips raw HTML (default is allow_html = True). if html_field_suffix is given, that value will be appended to the field name to generate the name of the non-editable HTML cache field. Default value is "_html". NOTE: The MarkdownTextField is not able to check whether the model defines any other fields with the same name as the HTML field it attempts to add - if there are other fields with this name, a database duplicate column error will be raised. """ def __init__(self, *args, **kwargs): self._markdown_safe = not kwargs.pop('allow_html', True) self._add_html_field = kwargs.pop('add_html_field', True) self._html_field_suffix = kwargs.pop('html_field_suffix', '_html') super(MarkdownTextField, self).__init__(*args, **kwargs) def contribute_to_class(self, cls, name): # During migrations, the column will already be in the new schema, # so we need to skip adding the column. The deconstruct serialization # will set _add_html_field to False for us in this case if self._add_html_field: self._html_field = "%s%s" % (name, self._html_field_suffix) TextField(editable=False).contribute_to_class(cls, self._html_field) super(MarkdownTextField, self).contribute_to_class(cls, name) def pre_save(self, model_instance, add): value = getattr(model_instance, self.attname) html = markdown(value, safe_mode=self._markdown_safe) setattr(model_instance, self._html_field, html) return value def deconstruct(self): # Django 1.7 migration serializer name, path, args, kwargs = super(MarkdownTextField, self).deconstruct() kwargs['add_html_field'] = False kwargs['allow_html'] = self._markdown_safe kwargs['html_field_suffix'] = self._html_field_suffix return name, path, args, kwargs def __unicode__(self): return unicode(self.attname) def __str__(self): return self.attname PK(HYwafer/kv/urls.pyfrom django.conf.urls import patterns, url, include from rest_framework import routers from wafer.kv.views import KeyValueViewSet router = routers.DefaultRouter() router.register(r'kv', KeyValueViewSet) urlpatterns = patterns( '', url(r'^api/', include(router.urls)), ) PK(H9""wafer/kv/views.pyfrom rest_framework import viewsets from wafer.kv.models import KeyValue from wafer.kv.serializers import KeyValueSerializer from wafer.kv.permissions import KeyValueGroupPermission class KeyValueViewSet(viewsets.ModelViewSet): """API endpoint that allows key-value pairs to be viewed or edited.""" queryset = KeyValue.objects.none() # Needed for the REST Permissions serializer_class = KeyValueSerializer permission_classes = (KeyValueGroupPermission, ) def get_queryset(self): # Restrict the list to only those that match the user's # groups if self.request.user.id is not None: grp_ids = [x.id for x in self.request.user.groups.all()] return KeyValue.objects.filter(group_id__in=grp_ids) return KeyValue.objects.none() PK}HVewafer/kv/utils.pyfrom django import forms from django.utils.dateparse import parse_date, parse_datetime, parse_time def deserialize_by_field(value, field): """ Some types get serialized to JSON, as strings. If we know what they are supposed to be, we can deserialize them """ if isinstance(field, forms.DateTimeField): value = parse_datetime(value) elif isinstance(field, forms.DateField): value = parse_date(value) elif isinstance(field, forms.TimeField): value = parse_time(value) return value PK}H}ehhwafer/kv/models.pyfrom django.contrib.auth.models import Group from django.db import models from jsonfield import JSONField class KeyValue(models.Model): group = models.ForeignKey(Group) key = models.CharField(max_length=64, db_index=True) value = JSONField() def __unicode__(self): return u'KV(%s, %s, %r)' % (self.group.name, self.key, self.value) PK}Hwafer/kv/__init__.pyPK(H|wafer/kv/permissions.pyfrom rest_framework.permissions import BasePermission class KeyValueGroupPermission(BasePermission): """Restrict access to a given key / value pair to members of the corresponding group.""" # We perhaps unwisely assume that the view set checks ensure we # aren't exposing other groups key-value combinations, so # we only need to provide the object permission check def has_object_permission(self, request, view, obj): # Only allow any sort of access if the user is a member of # the appropriate group group = obj.group user = request.user if user.groups.filter(name=group.name).exists(): if request.method in ['PUT', 'PATCH']: # XXX: Better ideas here? if 'group' in request.data and obj.group.pk != int(request.data['group']): self.message = "Cannot change the group owning this KeyValue pair" return False return True return False PK(H*-wafer/kv/serializers.pyfrom django.core.exceptions import PermissionDenied from rest_framework import serializers from wafer.kv.models import KeyValue class KeyValueSerializer(serializers.ModelSerializer): class Meta: model = KeyValue # There doesn't seem to be a better way of handling the problem # of filtering the groups. # See the DRF meta-issue https://github.com/tomchristie/django-rest-framework/issues/1985 # and various related subdisscussions, such as https://github.com/tomchristie/django-rest-framework/issues/2292 def __init__(self, *args, **kwargs): # Explicitly fail with a hopefully informative error message # if there is no request. This is for introspection tools which # call serializers without a request if 'request' not in kwargs['context']: raise PermissionDenied("No request information provided." "The KeyValue API isn't available without " "an authorized user") user = kwargs['context']['request'].user # Limit to groups shown to those we're a member of groups = self.fields['group'] groups.queryset = user.groups super(KeyValueSerializer, self).__init__(*args, **kwargs) PK(H%%wafer/kv/tests/test_kv_api.py"""Tests for wafer.kv api views.""" from django.contrib.auth import get_user_model from django.contrib.auth.models import Group from django.test import Client, TestCase from rest_framework.test import APIClient from wafer.kv.models import KeyValue import json def get_group(group): return Group.objects.get(name=group) def create_group(group): return Group.objects.create(name=group) def create_user(username, groups): create = get_user_model().objects.create_user user = create( username, '%s@example.com' % username, 'password') for the_group in groups: grp = get_group(the_group) user.groups.add(grp) user.save() return user def create_kv_pair(name, value, group): group = get_group(group) return KeyValue.objects.create(key=name, value=value, group=group) class KeyValueViewSetTests(TestCase): """Various tests of the API views.""" def setUp(self): for grp in ['group_1', 'group_2', 'group_3', 'group_4']: create_group(grp) self.user1 = create_user("user1", ["group_1", "group_2"]) self.user2 = create_user("user2", ["group_1"]), self.user3 = create_user("user3", ["group_4"]), self.user4 = create_user("user4", ["group_3"]), self.kv_1_grp1 = create_kv_pair("Val 1.1", '{"key": "Data"}', "group_1") self.kv_2_grp2 = create_kv_pair("Val 2.1", '{"key2": "False"}', "group_2") self.kv_2_grp1 = create_kv_pair("Val 1.2", '{"key3": "True"}', "group_1") self.kv_3_grp1 = create_kv_pair("Val 1.3", '{"key1": "Data"}', "group_1") self.kv_3_grp3 = create_kv_pair("Val 3.1", '{"key1": "Data"}', "group_3") self.duplicate_name = create_kv_pair("Val 1.1", '{"key1": "Data"}', "group_3") self.client = Client() def test_unauthorized_users(self): response = self.client.get('/kv/api/kv/') self.assertEqual(response.data['count'], 0) response = self.client.get('/kv/api/kv/%d/' % self.kv_1_grp1.pk) self.assertEqual(response.status_code, 404) def test_group_1_member(self): self.client.login(username='user2', password='password') response = self.client.get('/kv/api/kv/') self.assertEqual(response.data['count'], 3) pairs = [x['key'] for x in response.data['results']] self.assertTrue('Val 1.1' in pairs) self.assertTrue('Val 1.2' in pairs) self.assertTrue('Val 1.3' in pairs) response = self.client.get('/kv/api/kv/%d/' % self.kv_1_grp1.pk) self.assertEqual(response.data['key'], 'Val 1.1') response = self.client.get('/kv/api/kv/%d/' % self.kv_2_grp1.pk) self.assertEqual(response.data['key'], 'Val 1.2') response = self.client.get('/kv/api/kv/%d/' % self.kv_2_grp2.pk) self.assertEqual(response.status_code, 404) def test_group_1_2_member(self): self.client.login(username='user1', password='password') response = self.client.get('/kv/api/kv/') self.assertEqual(response.data['count'], 4) pairs = [x['key'] for x in response.data['results']] self.assertTrue('Val 1.1' in pairs) self.assertTrue('Val 1.2' in pairs) self.assertTrue('Val 1.3' in pairs) self.assertTrue('Val 2.1' in pairs) response = self.client.get('/kv/api/kv/%d/' % self.kv_1_grp1.pk) self.assertEqual(response.data['key'], 'Val 1.1') response = self.client.get('/kv/api/kv/%d/' % self.kv_2_grp1.pk) self.assertEqual(response.data['key'], 'Val 1.2') response = self.client.get('/kv/api/kv/%d/' % self.kv_2_grp2.pk) self.assertEqual(response.data['key'], 'Val 2.1') response = self.client.get('/kv/api/kv/%d/' % self.kv_3_grp3.pk) self.assertEqual(response.status_code, 404) def test_group_4_member(self): self.client.login(username='user3', password='password') response = self.client.get('/kv/api/kv/') self.assertEqual(response.data['count'], 0) response = self.client.get('/kv/api/kv/%d/' % self.kv_1_grp1.pk) self.assertEqual(response.status_code, 404) response = self.client.get('/kv/api/kv/%d/' % self.kv_2_grp2.pk) self.assertEqual(response.status_code, 404) response = self.client.get('/kv/api/kv/%d/' % self.kv_3_grp3.pk) self.assertEqual(response.status_code, 404) def test_group_3_member(self): self.client.login(username='user4', password='password') response = self.client.get('/kv/api/kv/') self.assertEqual(response.data['count'], 2) pairs = [x['key'] for x in response.data['results']] self.assertTrue('Val 1.1' in pairs) self.assertTrue('Val 3.1' in pairs) response = self.client.get('/kv/api/kv/%d/' % self.kv_3_grp3.pk) self.assertEqual(response.data['key'], 'Val 3.1') response = self.client.get('/kv/api/kv/%d/' % self.duplicate_name.pk) self.assertEqual(response.data['key'], 'Val 1.1') response = self.client.get('/kv/api/kv/%d/' % self.kv_1_grp1.pk) self.assertEqual(response.status_code, 404) class KeyValueAPITests(TestCase): """Test creating, updating and deleting via the api.""" def setUp(self): for grp in ['group_1', 'group_2']: create_group(grp) self.user1 = create_user("user1", ["group_1", "group_2"]) self.user2 = create_user("user2", ["group_1"]), self.user3 = create_user("user3", ["group_2"]), self.kv_1_grp1 = create_kv_pair("Val 1.1", '{"key": "Data"}', "group_1") self.kv_2_grp2 = create_kv_pair("Val 2.1", '{"key2": "False"}', "group_2") self.kv_3_grp1 = create_kv_pair("Val 1.3", '{"key1": "Data"}', "group_1") self.client = APIClient() def test_group_1_actions(self): self.client.login(username='user2', password='password') # Test creation data = {'key': 'new', 'value': "{'mykey': 'Value'}", 'group': get_group("group_1").pk} response = self.client.post('/kv/api/kv/', data, format='json') self.assertEqual(response.status_code, 201) self.assertTrue(KeyValue.objects.filter(key="new").exists()) kv = KeyValue.objects.get(key='new') self.assertEqual(kv.value, "{'mykey': 'Value'}") # Test update data = {'value': "{'mykey': 'Value 2'}"} response = self.client.patch('/kv/api/kv/%d/' % kv.pk, data, format='json') self.assertEqual(response.status_code, 200) kv = KeyValue.objects.get(key='new') self.assertEqual(kv.value, "{'mykey': 'Value 2'}") self.client.login(username='user1', password='password') # Test that changing group ownership fails data = {'group': get_group("group_2").pk} response = self.client.patch('/kv/api/kv/%d/' % kv.pk, data, format='json') self.assertEqual(response.status_code, 403) # Test deletion response = self.client.delete('/kv/api/kv/%d/' % kv.pk) self.assertEqual(response.status_code, 204) self.assertFalse(KeyValue.objects.filter(key="new").exists()) # Test non-group member self.client.login(username='user3', password='password') response = self.client.delete('/kv/api/kv/%d/' % self.kv_1_grp1.pk) self.assertEqual(response.status_code, 404) data = {'key': 'foobar'} response = self.client.patch('/kv/api/kv/%d/' % self.kv_1_grp1.pk, data, format='json') self.assertEqual(response.status_code, 404) # Test that we can't create keys outside our group data = {'key': 'new', 'value': "{'mykey': 'Value'}", 'group': get_group("group_1").pk} response = self.client.post('/kv/api/kv/', data, format='json') self.assertEqual(response.status_code, 400) def test_group_2_actions(self): # Same tests as above, but with group_2 and a different ordering # of users belonging to 1 or both groups # Multi-group user self.client.login(username='user1', password='password') data = {'key': 'new', 'value': "{'mykey': 'Value'}", 'group': get_group("group_2").pk} response = self.client.post('/kv/api/kv/', data, format='json') self.assertEqual(response.status_code, 201) self.assertTrue(KeyValue.objects.filter(key="new").exists()) kv = KeyValue.objects.get(key='new') self.assertEqual(kv.value, "{'mykey': 'Value'}") # Single group user self.client.login(username='user3', password='password') data = {'value': "{'mykey': 'Value 2'}"} response = self.client.patch('/kv/api/kv/%d/' % kv.pk, data, format='json') self.assertEqual(response.status_code, 200) kv = KeyValue.objects.get(key='new') self.assertEqual(kv.value, "{'mykey': 'Value 2'}") response = self.client.delete('/kv/api/kv/%d/' % kv.pk) self.assertEqual(response.status_code, 204) self.assertFalse(KeyValue.objects.filter(key="new").exists()) self.client.login(username='user2', password='password') response = self.client.delete('/kv/api/kv/%d/' % self.kv_2_grp2.pk) self.assertEqual(response.status_code, 404) data = {'key': 'foobar'} response = self.client.patch('/kv/api/kv/%d/' % self.kv_2_grp2.pk, data, format='json') self.assertEqual(response.status_code, 404) PK(HML%#wafer/kv/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models from django.conf import settings import jsonfield.fields class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL) ] operations = [ migrations.CreateModel( name='KeyValue', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('key', models.CharField(max_length=64, db_index=True)), ('value', jsonfield.fields.JSONField()), ('group', models.ForeignKey(to='auth.Group')), ], ), ] PK(Hwafer/kv/migrations/__init__.pyPKyIMRBBwafer/tickets/urls.pyfrom django.conf.urls import patterns, url from wafer.tickets.views import ClaimView urlpatterns = patterns( 'wafer.tickets.views', url(r'^claim/$', ClaimView.as_view(), name='wafer_ticket_claim'), url(r'^zapier_guest_hook/$', 'zapier_guest_hook'), url(r'^zapier_cancel_hook/$', 'zapier_cancel_hook'), ) PKyI.0[wafer/tickets/views.pyimport json import logging from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import PermissionDenied, ValidationError from django.core.urlresolvers import reverse from django.http import HttpResponse, Http404 from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST from django.views.generic.edit import FormView from wafer.utils import LoginRequiredMixin from wafer.tickets.models import Ticket, TicketType from wafer.tickets.forms import TicketForm log = logging.getLogger(__name__) class ClaimView(LoginRequiredMixin, FormView): template_name = 'wafer.tickets/claim.html' form_class = TicketForm def get_context_data(self, **kwargs): context = super(ClaimView, self).get_context_data(**kwargs) context['can_claim'] = self.can_claim() return context def can_claim(self): if settings.WAFER_REGISTRATION_MODE != 'ticket': raise Http404('Ticket-based registration is not in use') if not settings.WAFER_REGISTRATION_OPEN: return False return not self.request.user.userprofile.is_registered() def form_valid(self, form): if not self.can_claim(): raise ValidationError('User may not claim a ticket') ticket = Ticket.objects.get(barcode=form.cleaned_data['barcode']) ticket.user = self.request.user ticket.save() return super(ClaimView, self).form_valid(form) def get_success_url(self): return reverse( 'wafer_user_profile', args=(self.request.user.username,)) # We assume that the system is using quicket's Zapier integration, turning # Quicket events into web posts via the Zapier webhook endpoint. # For Zapier, we assume a shared secret has been set in the X-Zapier-Secret # header @csrf_exempt @require_POST def zapier_cancel_hook(request): ''' Zapier can post something like this when tickets are cancelled { "ticket_type": "Individual (Regular)", "barcode": "12345678", "email": "demo@example.com" } ''' if request.META.get('HTTP_X_ZAPIER_SECRET', None) != settings.WAFER_TICKETS_SECRET: raise PermissionDenied('Incorrect secret') # This is required for python 3, and in theory fine on python 2 payload = json.loads(request.body.decode('utf8')) ticket = Ticket.objects.filter(barcode=payload['barcode']) if ticket.exists(): # delete the ticket ticket.delete() return HttpResponse("Cancelled\n", content_type='text/plain') # We assume this is connected to the Quicket's 'guest added' Zapier # event. # Same considerations about the secret apply @csrf_exempt @require_POST def zapier_guest_hook(request): ''' Zapier can POST something like this when tickets are bought: { "ticket_type": "Individual (Regular)", "barcode": "12345678", "email": "demo@example.com" } ''' if request.META.get('HTTP_X_ZAPIER_SECRET', None) != settings.WAFER_TICKETS_SECRET: raise PermissionDenied('Incorrect secret') # This is required for python 3, and in theory fine on python 2 payload = json.loads(request.body.decode('utf8')) import_ticket(payload['barcode'], payload['ticket_type'], payload['email']) return HttpResponse("Noted\n", content_type='text/plain') def import_ticket(ticket_barcode, ticket_type, email): if Ticket.objects.filter(barcode=ticket_barcode).exists(): log.debug('Ticket already registered: %s', ticket_barcode) return # truncate long ticket type names to length allowed by database ticket_type = ticket_type[:TicketType.MAX_NAME_LENGTH] type_, created = TicketType.objects.get_or_create(name=ticket_type) UserModel = get_user_model() try: user = UserModel.objects.get(email=email, ticket=None) except UserModel.DoesNotExist: user = None except UserModel.MultipleObjectsReturned: # We're can't uniquely identify the user to associate this ticket # with, so leave it for them to figure out via the 'claim ticket' # interface user = None ticket = Ticket.objects.create( barcode=ticket_barcode, email=email, type=type_, user=user, ) ticket.save() if user: log.info('Ticket registered: %s and linked to user', ticket) else: log.info('Ticket registered: %s. Unclaimed', ticket) PKOIGUlAAwafer/tickets/forms.pyfrom crispy_forms.helper import FormHelper from crispy_forms.layout import Submit from django import forms from django.utils.translation import ugettext as _ from django.core.exceptions import ValidationError from wafer.tickets.models import Ticket class TicketForm(forms.Form): barcode = forms.fields.IntegerField(label='Ticket barcode') def __init__(self, *args, **kwargs): super(TicketForm, self).__init__(*args, **kwargs) self.helper = FormHelper() self.helper.add_input(Submit('submit', _('Claim'))) def clean_barcode(self): try: ticket = Ticket.objects.get(barcode=self.cleaned_data['barcode']) except Ticket.DoesNotExist: raise ValidationError(_( "There is no ticket with that barcode that's we're aware of " "being sold, yet. Please check it, and if correct, contact us." )) if ticket.user: raise ValidationError(_( "This ticket has already been claimed, by another user.")) return self.cleaned_data['barcode'] PKOIG{)wafer/tickets/models.pyfrom django.db import models from django.conf import settings from django.utils.encoding import python_2_unicode_compatible @python_2_unicode_compatible class TicketType(models.Model): MAX_NAME_LENGTH = 255 name = models.CharField(max_length=MAX_NAME_LENGTH) def __str__(self): return self.name @python_2_unicode_compatible class Ticket(models.Model): barcode = models.IntegerField(primary_key=True) email = models.EmailField(blank=True) type = models.ForeignKey(TicketType) user = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='ticket', blank=True, null=True, on_delete=models.SET_NULL) def __str__(self): return u'%s (%s)' % (self.barcode, self.email) PKOIGwafer/tickets/__init__.pyPKOIGSwafer/tickets/admin.pyfrom django.contrib import admin from wafer.tickets.models import Ticket, TicketType admin.site.register(Ticket) admin.site.register(TicketType) PK}Hyc3a0wafer/tickets/templates/wafer.tickets/claim.html{% extends 'wafer/base.html' %} {% load i18n %} {% load crispy_forms_tags %} {% block content %} {% if not WAFER_REGISTRATION_OPEN %} {% trans "Ticket sales are not currently open" %} {% elif not can_claim %} {% trans "Unable to claim tickets. Maybe you already have one?" %} {% else %}

    {% trans "Registration" %}

    {% trans "Registration is via Quicket." %}

    {#TODO INSERT Quicket widget HERE #}

    {% trans "Ticket claim" %}

    {% blocktrans %} Once you have registered, claim your ticket here, to confirm your registration. {% endblocktrans %}

    {% crispy form %}
    {% endif %} {% endblock %} PKOIG$wafer/tickets/management/__init__.pyPKOIG?hf>wafer/tickets/management/commands/import_quicket_guest_list.pyimport csv import logging from django.core.management.base import BaseCommand, CommandError from wafer.tickets.views import import_ticket class Command(BaseCommand): args = '' help = "Import a guest list CSV from Quicket" def handle(self, *args, **options): if len(args) != 1: raise CommandError('1 CSV File required') logging.basicConfig(level=logging.INFO) columns = ('Ticket Number', 'Ticket Barcode', 'Purchase Date', 'Ticket Type', 'Ticket Holder', 'Email', 'Cellphone', 'Checked in', 'Checked in date', 'Checked in by', 'Complimentary') keys = [column.lower().replace(' ', '_') for column in columns] with open(args[0], 'r') as f: reader = csv.reader(f) header = tuple(next(reader)) if header != columns: raise CommandError('CSV format has changed. Update wafer') for row in reader: ticket = dict(zip(keys, row)) import_ticket(ticket['ticket_barcode'], ticket['ticket_type'], ticket['email']) PKOIG-wafer/tickets/management/commands/__init__.pyPKOIGwafer/tickets/tests/__init__.pyPKyI !wafer/tickets/tests/test_views.pyimport json from django.test import TestCase, Client from django.contrib.auth import get_user_model from wafer.tickets.views import import_ticket from wafer.tickets.models import Ticket, TicketType class ImportTicketTests(TestCase): def setUp(self): self.user_email = "user@example.com" def create_user(self, email, user_name="User Foo"): UserModel = get_user_model() return UserModel.objects.create_user(user_name, email=email) def test_simple_import_with_user(self): user = self.create_user(self.user_email) import_ticket(12345, "Individual", self.user_email) ticket_type = TicketType.objects.get(name="Individual") ticket = Ticket.objects.get(barcode=12345) self.assertEqual(ticket.barcode, 12345) self.assertEqual(ticket.email, self.user_email) self.assertEqual(ticket.type, ticket_type) self.assertEqual(ticket.user, user) def test_simple_import_without_user(self): import_ticket(12345, "Individual", self.user_email) ticket_type = TicketType.objects.get(name="Individual") ticket = Ticket.objects.get(barcode=12345) self.assertEqual(ticket.barcode, 12345) self.assertEqual(ticket.email, self.user_email) self.assertEqual(ticket.type, ticket_type) self.assertEqual(ticket.user, None) def test_long_ticket_type(self): long_type = "Foo" * TicketType.MAX_NAME_LENGTH truncated_type = long_type[:TicketType.MAX_NAME_LENGTH] import_ticket(12345, long_type, self.user_email) ticket_type = TicketType.objects.get(name=truncated_type) ticket = Ticket.objects.get(barcode=12345) self.assertEqual(ticket.barcode, 12345) self.assertEqual(ticket.email, self.user_email) self.assertEqual(ticket.type, ticket_type) self.assertEqual(ticket.user, None) def test_ticket_barcode_already_exists(self): ticket_type = TicketType.objects.create(name="Test Type") initial_ticket = Ticket.objects.create( barcode=12345, email=self.user_email, type=ticket_type, user=None) import_ticket(12345, "Test Type", self.user_email) [ticket] = Ticket.objects.all() self.assertEqual(ticket, initial_ticket) class PostTicketTests(TestCase): """Test posting data to the web hook""" def test_posts(self): UserModel = get_user_model() email = "post@example.com" user = UserModel.objects.create_user('Joe', email=email) client = Client() post_data = { "ticket_type": "Test Type", "barcode": "54321", "email": "post@example.com" } with self.settings(WAFER_TICKETS_SECRET='testsecret'): # Check that the secret matters response = client.post('/tickets/zapier_guest_hook/', json.dumps(post_data), content_type="application/json", HTTP_X_ZAPIER_SECRET='wrongsecret') self.assertEqual(response.status_code, 403) # Check that the ticket gets processed correctly with an # existing user response = client.post('/tickets/zapier_guest_hook/', json.dumps(post_data), content_type="application/json", HTTP_X_ZAPIER_SECRET='testsecret') self.assertEqual(response.status_code, 200) ticket = Ticket.objects.get(barcode=54321) self.assertEqual(ticket.barcode, 54321) self.assertEqual(ticket.email, email) self.assertEqual(ticket.user, user) # Check duplicate post doesn't change anything response = client.post('/tickets/zapier_guest_hook/', json.dumps(post_data), content_type="application/json", HTTP_X_ZAPIER_SECRET='testsecret') self.assertEqual(response.status_code, 200) ticket = Ticket.objects.get(barcode=54321) self.assertEqual(ticket.barcode, 54321) self.assertEqual(ticket.email, email) self.assertEqual(ticket.user, user) # Change email to one that doesn't exist post_data['email'] = 'none@example.com' post_data['barcode'] = 65432 response = client.post('/tickets/zapier_guest_hook/', json.dumps(post_data), content_type="application/json", HTTP_X_ZAPIER_SECRET='testsecret') self.assertEqual(response.status_code, 200) ticket = Ticket.objects.get(barcode=65432) self.assertEqual(ticket.barcode, 65432) self.assertEqual(ticket.email, 'none@example.com') self.assertEqual(ticket.user, None) # Test cancelation response = client.post('/tickets/zapier_cancel_hook/', json.dumps(post_data), content_type="application/json", HTTP_X_ZAPIER_SECRET='testsecret') self.assertEqual(response.status_code, 200) # Check ticket has been deleted self.assertFalse(Ticket.objects.filter(barcode=65432).exists()) # Check earlier ticket still exists self.assertEqual(Ticket.objects.filter(barcode=54321).count(), 1) PK}H 3wafer/tickets/migrations/0003_longer_email_field.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('tickets', '0002_auto_20150813_1926'), ] operations = [ migrations.AlterField( model_name='ticket', name='email', field=models.EmailField(max_length=254, blank=True), ), ] PKOIGc~(wafer/tickets/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import django.db.models.deletion from django.conf import settings class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ migrations.CreateModel( name='Ticket', fields=[ ('barcode', models.IntegerField(serialize=False, primary_key=True)), ('email', models.EmailField(max_length=75, blank=True)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='TicketType', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(max_length=32)), ], options={ }, bases=(models.Model,), ), migrations.AddField( model_name='ticket', name='type', field=models.ForeignKey(to='tickets.TicketType'), preserve_default=True, ), migrations.AddField( model_name='ticket', name='user', field=models.ForeignKey(related_name='ticket', on_delete=django.db.models.deletion.SET_NULL, blank=True, to=settings.AUTH_USER_MODEL, null=True), preserve_default=True, ), ] PKOIG$wafer/tickets/migrations/__init__.pyPKOIGp3wafer/tickets/migrations/0002_auto_20150813_1926.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ('tickets', '0001_initial'), ] operations = [ migrations.AlterField( model_name='tickettype', name='name', field=models.CharField(max_length=255), ), ] PK|G:wafer/schedule/urls.pyfrom django.conf.urls import include, patterns, url from rest_framework import routers from wafer.schedule.views import ( CurrentView, ScheduleView, ScheduleItemViewSet, ScheduleXmlView, VenueView) router = routers.DefaultRouter() router.register(r'scheduleitems', ScheduleItemViewSet) urlpatterns = patterns( '', url(r'^$', ScheduleView.as_view(), name='wafer_full_schedule'), url(r'^venue/(?P\d+)/$', VenueView.as_view(), name='wafer_venue'), url(r'^current/$', CurrentView.as_view(), name='wafer_current'), url(r'^pentabarf\.xml$', ScheduleXmlView.as_view(), name='wafer_pentabarf_xml'), url(r'^api/', include(router.urls)), ) PKH;))wafer/schedule/views.pyimport datetime from django.views.generic import DetailView, TemplateView from rest_framework import viewsets from rest_framework.permissions import IsAdminUser from wafer.pages.models import Page from wafer.schedule.models import Venue, Slot, Day from wafer.schedule.admin import check_schedule, validate_schedule from wafer.schedule.models import ScheduleItem from wafer.schedule.serializers import ScheduleItemSerializer from wafer.talks.models import ACCEPTED from wafer.talks.models import Talk class ScheduleRow(object): """This is a helpful containter for the schedule view to keep sanity""" def __init__(self, schedule_day, slot): self.schedule_day = schedule_day self.slot = slot self.items = {} def get_sorted_items(self): sorted_items = [] for venue in self.schedule_day.venues: if venue in self.items: sorted_items.append(self.items[venue]) return sorted_items def __repr__(self): """Debugging aid""" return '%s - %s' % (self.slot, self.get_sorted_items()) class ScheduleDay(object): """A helpful container for information a days in a schedule view.""" def __init__(self, day): self.day = day self.venues = list(day.venue_set.all()) self.rows = [] class VenueView(DetailView): template_name = 'wafer.schedule/venue.html' model = Venue def make_schedule_row(schedule_day, slot, seen_items): """Create a row for the schedule table.""" row = ScheduleRow(schedule_day, slot) skip = [] expanding = {} all_items = list(slot.scheduleitem_set .select_related('talk', 'page', 'venue') .all()) for item in all_items: if item in seen_items: # Inc rowspan seen_items[item]['rowspan'] += 1 # Note that we need to skip this during colspan checks skip.append(item.venue) continue scheditem = {'item': item, 'rowspan': 1, 'colspan': 1} row.items[item.venue] = scheditem seen_items[item] = scheditem if item.expand: expanding[item.venue] = [] empty = [] expanding_right = None for venue in schedule_day.venues: if venue in skip: # Nothing to see here continue if venue in expanding: item = row.items[venue] for empty_venue in empty: row.items.pop(empty_venue) item['colspan'] += 1 empty = [] expanding_right = item elif venue in row.items: empty = [] expanding_right = None elif expanding_right: expanding_right['colspan'] += 1 else: empty.append(venue) row.items[venue] = {'item': None, 'rowspan': 1, 'colspan': 1} return row def generate_schedule(today=None): """Helper function which creates an ordered list of schedule days""" # We create a list of slots and schedule items schedule_days = {} seen_items = {} for slot in Slot.objects.all().order_by('end_time', 'start_time', 'day'): day = slot.get_day() if today and day != today: # Restrict ourselves to only today continue schedule_day = schedule_days.get(day) if schedule_day is None: schedule_day = schedule_days[day] = ScheduleDay(day) row = make_schedule_row(schedule_day, slot, seen_items) schedule_day.rows.append(row) return sorted(schedule_days.values(), key=lambda x: x.day.date) class ScheduleView(TemplateView): template_name = 'wafer.schedule/full_schedule.html' def get_context_data(self, **kwargs): context = super(ScheduleView, self).get_context_data(**kwargs) # Check if the schedule is valid context['active'] = False if not check_schedule(): return context context['active'] = True day = self.request.GET.get('day', None) dates = dict([(x.date.strftime('%Y-%m-%d'), x) for x in Day.objects.all()]) # We choose to return the full schedule if given an invalid date day = dates.get(day, None) context['schedule_days'] = generate_schedule(day) return context class ScheduleXmlView(ScheduleView): template_name = 'wafer.schedule/penta_schedule.xml' content_type = 'application/xml' class CurrentView(TemplateView): template_name = 'wafer.schedule/current.html' def _parse_today(self, day): if day is None: day = str(datetime.date.today()) dates = dict([(x.date.strftime('%Y-%m-%d'), x) for x in Day.objects.all()]) if day not in dates: return None return ScheduleDay(dates[day]) def _parse_time(self, time): now = datetime.datetime.now().time() if time is None: return now try: return datetime.datetime.strptime(time, '%H:%M').time() except ValueError: pass return now def _add_note(self, row, note, overlap_note): for item in row.items.values(): if item['rowspan'] == 1: item['note'] = note else: # Must overlap with current slot item['note'] = overlap_note def _current_slots(self, schedule_day, time): today = schedule_day.day cur_slot, prev_slot, next_slot = None, None, None for slot in Slot.objects.all(): if slot.get_day() != today: continue if slot.get_start_time() <= time and slot.end_time > time: cur_slot = slot elif slot.end_time <= time: if not prev_slot or prev_slot.end_time < slot.end_time: prev_slot = slot elif slot.get_start_time() >= time: if not next_slot or next_slot.end_time > slot.end_time: next_slot = slot cur_rows = self._current_rows( schedule_day, cur_slot, prev_slot, next_slot) return cur_slot, cur_rows def _current_rows(self, schedule_day, cur_slot, prev_slot, next_slot): seen_items = {} rows = [] for slot in (prev_slot, cur_slot, next_slot): if slot: row = make_schedule_row(schedule_day, slot, seen_items) else: row = None rows.append(row) # Add styling hints. Needs to be after all the schedule rows are # created so the spans are set correctly if prev_slot: self._add_note(rows[0], 'complete', 'current') if cur_slot: self._add_note(rows[1], 'current', 'current') if next_slot: self._add_note(rows[2], 'forthcoming', 'current') return [r for r in rows if r] def get_context_data(self, **kwargs): context = super(CurrentView, self).get_context_data(**kwargs) # If the schedule is invalid, return a context with active=False context['active'] = False if not check_schedule(): return context # The schedule is valid, so add active=True and empty slots context['active'] = True context['slots'] = [] # Allow refresh time to be overridden context['refresh'] = self.request.GET.get('refresh', None) # If there are no items scheduled for today, return an empty slots list schedule_day = self._parse_today(self.request.GET.get('day', None)) if schedule_day is None: return context context['schedule_day'] = schedule_day # Allow current time to be overridden time = self._parse_time(self.request.GET.get('time', None)) cur_slot, current_rows = self._current_slots(schedule_day, time) context['cur_slot'] = cur_slot context['slots'].extend(current_rows) return context class ScheduleItemViewSet(viewsets.ModelViewSet): """ API endpoint that allows groups to be viewed or edited. """ queryset = ScheduleItem.objects.all() serializer_class = ScheduleItemSerializer permission_classes = (IsAdminUser, ) class ScheduleEditView(TemplateView): template_name = 'wafer.schedule/edit_schedule.html' def _slot_context(self, slot, venues): slot_context = { 'name': slot.name, 'start_time': slot.get_start_time(), 'end_time': slot.end_time, 'id': slot.id, 'venues': [] } for venue in venues: venue_context = { 'name': venue.name, 'id': venue.id, } for schedule_item in slot.scheduleitem_set.all(): if schedule_item.venue.name == venue.name: venue_context['scheduleitem_id'] = schedule_item.id if schedule_item.talk: talk = schedule_item.talk venue_context['title'] = talk.title venue_context['talk'] = talk if (schedule_item.page and not schedule_item.page.exclude_from_static): page = schedule_item.page venue_context['title'] = page.name venue_context['page'] = page slot_context['venues'].append(venue_context) return slot_context def get_context_data(self, day_id=None, **kwargs): context = super(ScheduleEditView, self).get_context_data(**kwargs) days = Day.objects.all() if day_id: day = days.get(id=day_id) else: day = days.first() accepted_talks = Talk.objects.filter(status=ACCEPTED) venues = Venue.objects.filter(days__in=[day]) slots = Slot.objects.all().select_related( 'day', 'previous_slot').prefetch_related( 'scheduleitem_set', 'slot_set').order_by( 'end_time', 'start_time', 'day') aggregated_slots = [] for slot in slots: if day != slot.get_day(): continue aggregated_slots.append(self._slot_context(slot, venues)) context['day'] = day context['venues'] = venues context['slots'] = aggregated_slots context['talks_all'] = accepted_talks context['talks_unassigned'] = accepted_talks.filter(scheduleitem=None) context['pages'] = Page.objects.all() context['days'] = days context['validation_errors'] = validate_schedule() return context PKH = self.end_time: raise ValidationError("Start time must be before end time") @python_2_unicode_compatible class ScheduleItem(models.Model): venue = models.ForeignKey(Venue, on_delete=models.PROTECT) # Items can span multiple slots (tutorials, etc). slots = models.ManyToManyField(Slot) talk = models.ForeignKey(Talk, null=True, blank=True) page = models.ForeignKey(Page, null=True, blank=True) details = MarkdownTextField( null=False, blank=True, help_text=_("Additional details (if required)")) notes = models.TextField( null=False, blank=True, help_text=_("Notes for the conference organisers")) css_class = models.CharField( max_length=128, null=False, blank=True, help_text=_("Custom css class for this schedule item")) expand = models.BooleanField( null=False, default=False, help_text=_("Expand to neighbouring venues")) def get_title(self): if self.talk: return self.talk.title elif self.page: return self.page.name elif self.details: return self.details return 'No title' def get_talk_css_class(self): """Talk type css class if it's available""" if self.talk: return self.talk.talk_type.css_class() # Fallback for pages return '' get_talk_css_class.short_description = 'Talk Type CSS class' def get_desc(self): if self.details: if self.talk: return '%s - %s' % (self.talk.title, self.details) return self.details elif self.talk: return self.talk.title elif self.page: return self.page.name return '' def get_url(self): if self.talk: return self.talk.get_absolute_url() elif self.page: return self.page.get_absolute_url() return None def get_details(self): return self.get_desc() def get_start_time(self): slots = list(self.slots.all()) if slots: start = slots[0].get_start_time().strftime('%H:%M') day = slots[0].get_day() return u'%s, %s' % (day, start) else: return 'WARNING: No Time and Day Specified' def __str__(self): return u'%s in %s at %s' % (self.get_desc(), self.venue, self.get_start_time()) def get_duration(self): """Return the total duration of the item. This is the sum of all the slot durations.""" # This is intended for the pentabarf xml file # It will do the wrong thing if the slots aren't # contigious, which we should address sometime. slots = list(self.slots.all()) result = {'hours': 0, 'minutes': 0} if slots: for slot in slots: dur = slot.get_duration() result['hours'] += dur['hours'] result['minutes'] += dur['minutes'] # Normalise again hours, result['minutes'] = divmod(result['minutes'], 60) result['hours'] += hours return result def get_duration_minutes(self): """Return the duration in total number of minutes.""" duration = self.get_duration() return int(duration['hours'] * 60 + duration['minutes']) def invalidate_check_schedule(*args, **kw): from wafer.schedule.admin import check_schedule check_schedule.invalidate() post_save.connect(invalidate_check_schedule, sender=Day) post_save.connect(invalidate_check_schedule, sender=Venue) post_save.connect(invalidate_check_schedule, sender=Slot) post_save.connect(invalidate_check_schedule, sender=ScheduleItem) post_delete.connect(invalidate_check_schedule, sender=Day) post_delete.connect(invalidate_check_schedule, sender=Venue) post_delete.connect(invalidate_check_schedule, sender=Slot) post_delete.connect(invalidate_check_schedule, sender=ScheduleItem) PKOIGadwafer/schedule/renderers.pyfrom django_medusa.renderers import StaticSiteRenderer from wafer.schedule.models import Venue class ScheduleRenderer(StaticSiteRenderer): def get_paths(self): paths = ["/schedule/", "/schedule/pentabarf.xml"] # Add the venues items = Venue.objects.all() for item in items: paths.append(item.get_absolute_url()) return paths renderers = [ScheduleRenderer, ] PKOIGwafer/schedule/__init__.pyPKH@g 0 0wafer/schedule/admin.pyimport datetime from django.conf.urls import url from django.contrib import admin from django.contrib import messages from django.utils.encoding import force_text from django.utils.translation import ugettext as _ from django import forms from wafer.schedule.models import Day, Venue, Slot, ScheduleItem from wafer.talks.models import Talk, ACCEPTED from wafer.pages.models import Page from wafer.utils import cache_result # These are functions to simplify testing def find_overlapping_slots(): """Find any slots that overlap""" overlaps = set([]) all_slots = list(Slot.objects.all()) for slot in all_slots: # Because slots are ordered, we can be more efficient than this # N^2 loop, but this is simple and, since the number of slots # should be low, this should be "fast enough" start = slot.get_start_time() end = slot.end_time for other_slot in all_slots: if other_slot.pk == slot.pk: continue if other_slot.get_day() != slot.get_day(): # different days, can't overlap continue # Overlap if the start_time or end_time is bounded by our times # start_time <= other.start_time < end_time # or # start_time < other.end_time <= end_time other_start = other_slot.get_start_time() other_end = other_slot.end_time if start <= other_start and other_start < end: overlaps.add(slot) overlaps.add(other_slot) elif start < other_end and other_end <= end: overlaps.add(slot) overlaps.add(other_slot) return overlaps def find_non_contiguous(all_items=None): """Find any items that have slots that aren't contiguous""" if all_items is None: all_items = prefetch_schedule_items() non_contiguous = [] for item in all_items: if item.slots.count() < 2: # No point in checking continue last_slot = None for slot in item.slots.all().order_by('end_time'): if last_slot: if last_slot.end_time != slot.get_start_time(): non_contiguous.append(item) break last_slot = slot return non_contiguous def validate_items(all_items=None): """Find errors in the schedule. Check for: - pending / rejected talks in the schedule - items with both talks and pages assigned - items with neither talks nor pages assigned """ if all_items is None: all_items = prefetch_schedule_items() validation = [] for item in all_items: if item.talk is not None and item.page is not None: validation.append(item) elif item.talk is None and item.page is None: validation.append(item) elif item.talk and item.talk.status != ACCEPTED: validation.append(item) return validation def find_duplicate_schedule_items(all_items=None): """Find talks / pages assigned to mulitple schedule items""" if all_items is None: all_items = prefetch_schedule_items() duplicates = [] seen_talks = {} for item in all_items: if item.talk and item.talk in seen_talks: duplicates.append(item) if seen_talks[item.talk] not in duplicates: duplicates.append(seen_talks[item.talk]) else: seen_talks[item.talk] = item # We currently allow duplicate pages for cases were we need disjoint # schedule items, like multiple open space sessions on different # days and similar cases. This may be revisited later return duplicates def find_clashes(all_items=None): """Find schedule items which clash (common slot and venue)""" if all_items is None: all_items = prefetch_schedule_items() clashes = {} seen_venue_slots = {} for item in all_items: for slot in item.slots.all(): pos = (item.venue, slot) if pos in seen_venue_slots: if seen_venue_slots[pos] not in clashes: clashes[pos] = [seen_venue_slots[pos]] clashes[pos].append(item) else: seen_venue_slots[pos] = item return clashes def find_invalid_venues(all_items=None): """Find venues assigned slots that aren't on the allowed list of days.""" if all_items is None: all_items = prefetch_schedule_items() venues = {} for item in all_items: valid = False item_days = list(item.venue.days.all()) for slot in item.slots.all(): for day in item_days: if day == slot.get_day(): valid = True break if not valid: venues.setdefault(item.venue, []) venues[item.venue].append(item) return venues def prefetch_schedule_items(): """Prefetch all schedule items and related objects.""" return list(ScheduleItem.objects .select_related( 'talk', 'page', 'venue') .prefetch_related( 'slots', 'slots__previous_slot', 'slots__day') .all()) @cache_result('wafer_schedule_check_schedule', 60*60) def check_schedule(): """Helper routine to easily test if the schedule is valid""" all_items = prefetch_schedule_items() if find_clashes(all_items): return False if find_duplicate_schedule_items(all_items): return False if validate_items(all_items): return False if find_overlapping_slots(): return False if find_non_contiguous(all_items): return False if find_invalid_venues(all_items): return False return True def validate_schedule(): """Helper routine to easily test if the schedule is valid""" all_items = prefetch_schedule_items() errors = [] if find_clashes(all_items): errors.append('Clashes found in schedule.') if find_duplicate_schedule_items(all_items): errors.append('Duplicate schedule items found in schedule.') if validate_items(all_items): errors.append('Invalid schedule items found in schedule.') if find_overlapping_slots(): errors.append('Overlapping slots found in schedule.') if find_non_contiguous(all_items): errors.append('Non contiguous slots found in schedule.') if find_invalid_venues(all_items): errors.append('Invalid venues found in schedule.') return errors class ScheduleItemAdminForm(forms.ModelForm): class Meta: model = ScheduleItem fields = ('slots', 'venue', 'talk', 'page', 'details', 'notes', 'css_class', 'expand') def __init__(self, *args, **kwargs): super(ScheduleItemAdminForm, self).__init__(*args, **kwargs) self.fields['talk'].queryset = Talk.objects.filter(status=ACCEPTED) # Present all pages as possible entries in the schedule self.fields['page'].queryset = Page.objects.all() class ScheduleItemAdmin(admin.ModelAdmin): form = ScheduleItemAdminForm change_list_template = 'admin/scheduleitem_list.html' readonly_fields = ('get_talk_css_class',) list_display = ('get_start_time', 'venue', 'get_title', 'expand') list_editable = ('expand',) # We stuff these validation results into the view, rather than # enforcing conditions on the actual model, since it can be hard # to edit the schedule and keep it entirely consistent at every # step (think exchanging talks and so forth) def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} # Find issues in the schedule all_items = None clashes = find_clashes() validation = validate_items(all_items) venues = find_invalid_venues() duplicates = find_duplicate_schedule_items(all_items) non_contiguous = find_non_contiguous(all_items) errors = {} if clashes: errors['clashes'] = clashes if duplicates: errors['duplicates'] = duplicates if validation: errors['validation'] = validation if venues: errors['venues'] = venues if non_contiguous: errors['non_contiguous'] = non_contiguous extra_context['errors'] = errors return super(ScheduleItemAdmin, self).changelist_view(request, extra_context) def get_urls(self): from wafer.schedule.views import ScheduleEditView urls = super(ScheduleItemAdmin, self).get_urls() admin_schedule_edit_view = self.admin_site.admin_view( ScheduleEditView.as_view()) my_urls = [ url(r'^edit/$', admin_schedule_edit_view, name='schedule_editor'), url(r'^edit/(?P[0-9]+)$', admin_schedule_edit_view, name='schedule_editor'), ] return my_urls + urls class SlotAdminForm(forms.ModelForm): class Meta: model = Slot fields = ('name', 'previous_slot', 'day', 'start_time', 'end_time') class Media: js = ('js/scheduledatetime.js',) class SlotAdminAddForm(SlotAdminForm): # Additional field added for creating multiple slots at once additional = forms.IntegerField(min_value=0, max_value=30, required=False, label=_("Additional slots"), help_text=_("Create this number of " "additional slots following" "this one")) class SlotAdmin(admin.ModelAdmin): form = SlotAdminForm list_display = ('__str__', 'day', 'end_time') list_editable = ('end_time',) change_list_template = 'admin/slot_list.html' def changelist_view(self, request, extra_context=None): extra_context = extra_context or {} # Find issues with the slots errors = {} overlaps = find_overlapping_slots() if overlaps: errors['overlaps'] = overlaps extra_context['errors'] = errors return super(SlotAdmin, self).changelist_view(request, extra_context) def get_form(self, request, obj=None, **kwargs): """Change the form depending on whether we're adding or editing the slot.""" if obj is None: # Adding a new Slot kwargs['form'] = SlotAdminAddForm return super(SlotAdmin, self).get_form(request, obj, **kwargs) def save_model(self, request, obj, form, change): super(SlotAdmin, self).save_model(request, obj, form, change) if not change and form.cleaned_data['additional'] > 0: # We add the requested additional slots # All created slot will have the same length as the slot just # created , and we specify them as a sequence using # "previous_slot" so tweaking start times is simple. prev = obj end = datetime.datetime.combine(prev.day.date, prev.end_time) start = datetime.datetime.combine(prev.day.date, prev.get_start_time()) slot_len = end - start for loop in range(form.cleaned_data['additional']): end = end + slot_len new_slot = Slot(day=prev.day, previous_slot=prev, end_time=end.time()) new_slot.save() msgdict = {'obj': force_text(new_slot)} msg = _("Additional slot %(obj)s added sucessfully") % msgdict if hasattr(request, '_messages'): # Don't add messages unless we have a suitable request # Needed during testing, and possibly in other cases self.message_user(request, msg, messages.SUCCESS) prev = new_slot admin.site.register(Day) admin.site.register(Slot, SlotAdmin) admin.site.register(Venue) admin.site.register(ScheduleItem, ScheduleItemAdmin) PKv|G<wafer/schedule/serializers.pyfrom rest_framework import serializers from wafer.talks.models import Talk from wafer.pages.models import Page from wafer.schedule.models import ScheduleItem, Venue, Slot class ScheduleItemSerializer(serializers.HyperlinkedModelSerializer): page = serializers.PrimaryKeyRelatedField( allow_null=True, queryset=Page.objects.all()) talk = serializers.PrimaryKeyRelatedField( allow_null=True, queryset=Talk.objects.all()) venue = serializers.PrimaryKeyRelatedField( allow_null=True, queryset=Venue.objects.all()) slots = serializers.PrimaryKeyRelatedField( allow_null=True, many=True, queryset=Slot.objects.all()) class Meta: model = ScheduleItem fields = ('id', 'talk', 'page', 'venue', 'slots') def create(self, validated_data): venue_id = validated_data['venue'] slots = validated_data['slots'] talk = validated_data.get('talk') page = validated_data.get('page') try: existing_schedule_item = ScheduleItem.objects.get( venue_id=venue_id, slots__in=slots) except ScheduleItem.DoesNotExist: pass else: existing_schedule_item.talk = talk existing_schedule_item.page = page existing_schedule_item.slots = slots existing_schedule_item.save() return existing_schedule_item return super(ScheduleItemSerializer, self).create(validated_data) PKOIGaw2wafer/schedule/templates/wafer.schedule/venue.html{% extends "wafer/base.html" %} {% block content %}

    {{ object.name }}

    {{ object.notes_html|safe }}
    {% endblock %} PKHrqyQ 4wafer/schedule/templates/wafer.schedule/current.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {% trans "Current" %}

    {% if not active %} {# Schedule is incomplete / invalid, so show nothing #} {% blocktrans %}

    The final schedule has not been published yet.

    {% endblocktrans %} {% elif not slots %} {% blocktrans %}

    Nothing happening right now.

    {% endblocktrans %} {% else %} {% for venue in schedule_day.venues %} {% endfor %} {% for row in slots %} {% if row.slot == cur_slot %} {% else %} {% endif %} {% for item in row.get_sorted_items %} {% if item.item == "unavailable" %} {% else %} {% if item.note == "complete" %} {% endif %} {% endfor %} {% endfor %}
    {{ schedule_day.day.date|date:"l (d b)" }}
    {% trans "Time" %}{{ venue.name }}
    {{ row.slot.get_start_time|time:"H:i" }} - {{ row.slot.end_time|time:"H:i" }} (Now On){{ row.slot.get_start_time|time:"H:i" }} - {{ row.slot.end_time|time:"H:i" }} {% elif item.note == "current" %} {% else %} {% endif %} {% include "wafer.schedule/schedule_item.html" with item=item.item %}
    {% endif %}
    Last updated: {% now "H:m:s" %}
    {% endblock %} {% block extra_foot %} {% if refresh %} {% endif %} {% endblock %} PKHKjj:wafer/schedule/templates/wafer.schedule/edit_schedule.html{% extends "wafer/base.html" %} {% block content %}
    {% if validation_errors %}
    {% for validation_error in validation_errors %}
    {{ validation_error }}
    {% endfor %}
    {% endif %}

    Schedule Editor

    {% for venue in venues %} {% endfor %} {% for slot in slots %} {% for venue in slot.venues %} {% endfor %} {% endfor %}
    {{ venue.name }}
    {{ slot.name }}
    {{ slot.start_time }} - {{ slot.end_time }}
    {% if venue.scheduleitem_id %} {% endif %} {{ venue.title }}

    Bucket

    {% for talk in talks_unassigned %} {{ talk.title|truncatechars:24 }} {% endfor %}
    {% for talk in talks_all %} {{ talk.title|truncatechars:24 }} {% endfor %}
    {% for page in pages %} > {{ page.name|truncatechars:24 }} {% endfor %}
    {% endblock %} {% block extra_foot %} {% endblock %} PK(HQ1:wafer/schedule/templates/wafer.schedule/penta_schedule.xml {% load i18n %} {{ WAFER_CONFERENCE_NAME }} {% if schedule_days %} {% with day=schedule_days|first %} {{ day.day.date|date:"Y-m-d" }} {% endwith %} {% with day=schedule_days|last %} {{ day.day.date|date:"Y-m-d" }} {% endwith %} {{ schedule_days|length }} {% endif %} 00:00 00:15 {% for schedule_day in schedule_days %} {% for venue in schedule_day.venues %} {% for row in schedule_day.rows %} {% if venue in row.items %} {# this is more than a little horrible, but will do for testing #} {% for row_venue, items in row.items.items %} {% if row_venue == venue %} {# The event id is the ScheduleItem pk, which should be unique enough #} {# Not sure what to do about timezones here #} {{ schedule_day.day.date|date:"Y-m-d" }}T{{ row.slot.get_start_time|time:"H:i:s" }}+00:00 {{ row.slot.get_start_time|time:"H:i" }} {% with dur=items.item.get_duration %} {{ dur.hours|stringformat:"02d" }}:{{ dur.minutes|stringformat:"02d" }} {% endwith %} {{ venue.name }} {# Confclerk needs this to import the conference. #} {# We set this to the same as the room name, since there doesn't seem a better choice #} {{ venue.name }} {# It's not clear what the difference between abstract and description is meant to be #} {# Both confclerk and Giggity just lump them together into the same thing anyway, and #} {# summit only outputs stuff in description, so we keep abstract blank and follow #} {# summit's pattern and obly include stuff in the description #} {% if items.item.talk %} {{ items.item.get_title }} {# I'm not sure if the raw markdown is the best thing here, but it seems to match what summit does, #} {# so hopefully the tools can handle the odd corner cases correctly. Giggity at least does some #} {# santization here. #} {% if items.item.talk.abstract %} {{ items.item.talk.abstract.raw }} {% else %} {% endif %} {{ items.item.talk.talk_type }} {% for author in items.item.talk.authors.all %} {# person id is the author pk, which should be the right thing #} {% if user.is_staff %} {# We will want finer grained control off this eventually, but staff will do for now #} {{ author.userprofile.display_name }} {% else %} {{ author.userprofile.display_name }} {% endif %} {% endfor %} {% else %} {{ items.item.get_details|escape }} {% if items.item.page.people.exists %} {# If there are people, we care about the description #} {# For now, we drop the raw markdown content from pages here. We probably want to sanatize this in the future #} {{ items.item.page.content.raw }} {# TODO: This should be refactored, so we don't have all this duplication #} {% for person in items.item.page.people.all %} {% if user.is_staff %} {# We will want finer grained control off this eventually, but staff will do for now #} {{ person.userprofile.display_name }} {% else %} {{ person.userprofile.display_name }} {% endif %} {% endfor %} {% else %} {% endif %} {% endif %} {{ items.item.get_url }} {# It's useful to have the full url available in the xml file. The pentabarf.xml format isn't that well #} {# standardised, so we add our own full_conf_url tag to accomodate this requirement #} {# forcing https here is a bit horrible - make this configurable somewhere? #} https://{{ WAFER_CONFERENCE_DOMAIN }}{{ items.item.get_url }} {# someday there may be a way to set this to False #} True {% endif %} {% endfor %} {% endif %} {% endfor %} {% endfor %} {% endfor %} PKcGt6r:wafer/schedule/templates/wafer.schedule/schedule_item.html{% if item.talk %} {{ item.get_details|escape }}
    by {{ item.talk.get_authors_display_name }} {% elif item.get_url %} {{ item.get_details|escape }} {% if item.page and item.page.people.exists %} by {{ item.page.get_people_display_names }} {% endif %} {% else %} {{ item.get_details|escape }} {% endif %} PKHO77:wafer/schedule/templates/wafer.schedule/full_schedule.html{% extends "wafer/base.html" %} {% load i18n %} {% block content %}

    {% trans "Schedule" %} {% if user.is_authenticated and user.is_staff %} {% endif %}

    {% if not schedule_days %} {# Schedule is incomplete / invalid, so show nothing #} {% blocktrans %}

    The final schedule has not been published yet.

    {% endblocktrans %} {% else %} {% for schedule_day in schedule_days %} {# We assume that the admin has created a valid timetable #} {% for venue in schedule_day.venues %} {% endfor %} {% for row in schedule_day.rows %} {% for item in row.get_sorted_items %} {% if item.item == "unavailable" %} {# Venue isn't available, so we add an empty table element with the 'unavailable' class #} {% else %} {# Add item details #} {% endif %} {% endfor %} {% endfor %}
    {{ schedule_day.day.date|date:"l (d b)" }}
    {% trans "Time" %}{{ venue.name }}
    {{ row.slot.get_start_time|time:"H:i" }} - {{ row.slot.end_time|time:"H:i" }} {% include "wafer.schedule/schedule_item.html" with item=item.item %}
    {% endfor %} {% endif %}
    {% endblock %} PKOIG qq-wafer/schedule/templates/admin/slot_list.html{% extends "admin/change_list.html" %} {% load i18n %} {# This plugs our extra validation information into the end of the content block #} {% block content %} {{ block.super }}
    {% if errors %}

    {% trans "Errors in the slots" %}

    {% if errors.overlaps %}

    {% trans "OVerlapping slots" %}

      {% for item in errors.overlaps %}
    • {{ item }}
    • {% endfor %}
    {% endif %}
    {% endif %}
    {% endblock %} PK(H `zz5wafer/schedule/templates/admin/scheduleitem_list.html{% extends "admin/change_list.html" %} {% load i18n %} {# This plugs our extra validation information into the end of the content block #} {% block content %} {{ block.super }}
    {% if errors %}

    {% trans "Errors in the schedule" %}

    {% if errors.clashes %}

    {% trans "Clashes" %}

      {% for pos, items in errors.clashes.items %}
    • {{ pos.0 }} at {{ pos.1.get_start_time|time:"H:i" }} -- {% for item in items %} {{ item }}, {% endfor %}
    • {% endfor %}
    {% endif %} {% if errors.validation %}

    {% trans "Validation errors" %}

      {% for item in errors.validation %}
    • {{ item }}
    • {% endfor %}
    {% endif %} {% if errors.non_contiguous %}

    {% trans "Items in the schedule with non-contiguous slots" %}

      {% for item in errors.non_contiguous %}
    • {{ item }}
    • {% endfor %}
    {% endif %} {% if errors.duplicates %}

    {% trans "Duplicates in the schedule" %}

      {% for item in errors.duplicates %}
    • {{ item }}
    • {% endfor %}
    {% endif %} {% if errors.venues %}

    {% trans "Venues assigned on days they are not available" %}

      {% for venue, items in errors.venues.items %}
    • {{ venue }} -- {% for item in items %} {{ item }}, {% endfor %}
    • {% endfor %}
    {% endif %}
    {% endif %}
    {% endblock %} {% block object-tools-items %} {{ block.super }}
  • Edit schedule
  • {% endblock %} PKOIG wafer/schedule/tests/__init__.pyPK|G0#wafer/schedule/tests/test_models.pyimport datetime as D from django.test import TestCase from wafer.schedule.models import Day class DayTests(TestCase): def test_days(self): """Create some days and check the results.""" Day.objects.create(date=D.date(2013, 9, 22)) Day.objects.create(date=D.date(2013, 9, 23)) assert Day.objects.count() == 2 output = ["%s" % x for x in Day.objects.all()] assert output == ["Sep 22 (Sun)", "Sep 23 (Mon)"] PK&GZݝEE"wafer/schedule/tests/test_admin.pyimport datetime as D from django.contrib.auth import get_user_model from django.test import TestCase from django.http import HttpRequest from wafer.pages.models import Page from wafer.schedule.admin import ( SlotAdmin, find_overlapping_slots, validate_items, find_duplicate_schedule_items, find_clashes, find_invalid_venues, find_non_contiguous) from wafer.schedule.models import Day, Venue, Slot, ScheduleItem from wafer.talks.models import Talk, ACCEPTED, REJECTED, PENDING class DummyForm(object): def __init__(self): self.cleaned_data = {} def make_dummy_form(additional): """Fake a form object for the tests""" form = DummyForm() form.cleaned_data['additional'] = additional return form class SlotAdminTests(TestCase): def setUp(self): """Create some Venues and Days for use in the actual tests.""" self.day = Day.objects.create(date=D.date(2013, 9, 22)) self.admin = SlotAdmin(Slot, None) def test_save_model_single_new(self): """Test save_model creating a new slot, but no additional slots""" slot = Slot(day=self.day, start_time=D.time(11, 0, 0), end_time=D.time(11, 30, 0)) # check that it's not saved in the database yet self.assertEqual(Slot.objects.count(), 0) request = HttpRequest() dummy = make_dummy_form(0) self.admin.save_model(request, slot, dummy, False) # check that it's now been saved in the database self.assertEqual(Slot.objects.count(), 1) slot2 = Slot.objects.filter(start_time=D.time(11, 0, 0)).get() self.assertEqual(slot, slot2) def test_save_model_change_slot(self): """Test save_model changing a slot""" slot = Slot(day=self.day, start_time=D.time(11, 0, 0), end_time=D.time(12, 30, 0)) # end_time is chosen as 12:30 so it stays valid through all the # subsequent fiddling slot.save() # check that it's saved in the database self.assertEqual(Slot.objects.count(), 1) request = HttpRequest() dummy = make_dummy_form(0) slot.start_time = D.time(12, 0, 0) self.assertEqual( Slot.objects.filter(start_time=D.time(11, 0, 0)).count(), 1) self.admin.save_model(request, slot, dummy, True) # Check that the database has changed self.assertEqual( Slot.objects.filter(start_time=D.time(11, 0, 0)).count(), 0) self.assertEqual(Slot.objects.count(), 1) slot2 = Slot.objects.filter(start_time=D.time(12, 0, 0)).get() self.assertEqual(slot, slot2) # Check that setting additional has no influence on the change path dummy = make_dummy_form(3) slot.start_time = D.time(11, 0, 0) self.assertEqual( Slot.objects.filter(start_time=D.time(11, 0, 0)).count(), 0) self.admin.save_model(request, slot, dummy, True) # Still only 1 object self.assertEqual(Slot.objects.count(), 1) # And it has been updated self.assertEqual( Slot.objects.filter(start_time=D.time(12, 0, 0)).count(), 0) self.assertEqual( Slot.objects.filter(start_time=D.time(11, 0, 0)).count(), 1) def test_save_model_new_additional(self): """Test save_model changing a new slot with some additional slots""" slot = Slot(day=self.day, start_time=D.time(11, 0, 0), end_time=D.time(11, 30, 0)) # check that it's not saved in the database self.assertEqual(Slot.objects.count(), 0) request = HttpRequest() dummy = make_dummy_form(3) self.admin.save_model(request, slot, dummy, False) self.assertEqual(Slot.objects.count(), 4) # check the hierachy is created correctly slot1 = Slot.objects.filter(previous_slot=slot).get() self.assertEqual(slot1.get_start_time(), slot.end_time) self.assertEqual(slot1.end_time, D.time(12, 0, 0)) slot2 = Slot.objects.filter(previous_slot=slot1).get() self.assertEqual(slot2.get_start_time(), slot1.end_time) self.assertEqual(slot2.end_time, D.time(12, 30, 0)) self.assertEqual(slot2.day, slot.day) slot3 = Slot.objects.filter(previous_slot=slot2).get() self.assertEqual(slot3.get_start_time(), slot2.end_time) self.assertEqual(slot3.end_time, D.time(13, 00, 0)) self.assertEqual(slot3.day, slot.day) # repeat checks with a different length of slot slot = Slot(day=self.day, previous_slot=slot3, end_time=D.time(14, 30, 0)) dummy = make_dummy_form(4) self.admin.save_model(request, slot, dummy, False) self.assertEqual(Slot.objects.count(), 9) slot1 = Slot.objects.filter(previous_slot=slot).get() self.assertEqual(slot1.get_start_time(), slot.end_time) self.assertEqual(slot1.end_time, D.time(16, 0, 0)) slot2 = Slot.objects.filter(previous_slot=slot1).get() self.assertEqual(slot2.get_start_time(), slot1.end_time) self.assertEqual(slot2.end_time, D.time(17, 30, 0)) self.assertEqual(slot2.day, slot.day) slot3 = Slot.objects.filter(previous_slot=slot2).get() self.assertEqual(slot3.get_start_time(), slot2.end_time) self.assertEqual(slot3.end_time, D.time(19, 00, 0)) self.assertEqual(slot3.day, slot.day) slot4 = Slot.objects.filter(previous_slot=slot3).get() self.assertEqual(slot4.get_start_time(), slot3.end_time) self.assertEqual(slot4.end_time, D.time(20, 30, 0)) self.assertEqual(slot4.day, slot.day) class ValidationTests(TestCase): def test_slot(self): """Test detection of overlapping slots""" day1 = Day.objects.create(date=D.date(2013, 9, 22)) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) start35 = D.time(12, 30, 0) start4 = D.time(13, 0, 0) start45 = D.time(13, 30, 0) start5 = D.time(14, 0, 0) end = D.time(15, 0, 0) # Test common start time slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start1, end_time=end, day=day1) overlaps = find_overlapping_slots() assert overlaps == set([slot1, slot2]) slot2.start_time = start5 slot2.save() # Test interleaved slot slot3 = Slot.objects.create(start_time=start2, end_time=start3, day=day1) slot4 = Slot.objects.create(start_time=start4, end_time=start5, day=day1) slot5 = Slot.objects.create(start_time=start35, end_time=start45, day=day1) overlaps = find_overlapping_slots() assert overlaps == set([slot4, slot5]) # Test no overlap slot5.start_time = start3 slot5.end_time = start4 slot5.save() overlaps = find_overlapping_slots() assert len(overlaps) == 0 # Test common end time slot5.end_time = start5 slot5.save() overlaps = find_overlapping_slots() assert overlaps == set([slot4, slot5]) # Test overlap detect with previous slot set slot5.start_time = None slot5.end_time = start5 slot5.previous_slot = slot1 slot5.save() overlaps = find_overlapping_slots() assert overlaps == set([slot3, slot4, slot5]) def test_clashes(self): """Test that we can detect clashes correctly""" day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue2 = Venue.objects.create(order=2, name='Venue 2') venue1.days.add(day1) venue2.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) end = D.time(12, 0, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=end, day=day1) item1 = ScheduleItem.objects.create(venue=venue1, details="Item 1") item2 = ScheduleItem.objects.create(venue=venue1, details="Item 2") # Create a simple venue/slot clash item1.slots.add(slot1) item2.slots.add(slot1) clashes = find_clashes() assert len(clashes) == 1 pos = (venue1, slot1) assert pos in clashes assert item1 in clashes[pos] assert item2 in clashes[pos] # Create a overlapping clashes item2.slots.remove(slot1) item1.slots.add(slot2) item2.slots.add(slot2) clashes = find_clashes() assert len(clashes) == 1 pos = (venue1, slot2) assert pos in clashes assert item1 in clashes[pos] assert item2 in clashes[pos] # Add a clash in a second venue item3 = ScheduleItem.objects.create(venue=venue2, details="Item 3") item4 = ScheduleItem.objects.create(venue=venue2, details="Item 4") item3.slots.add(slot2) item4.slots.add(slot2) clashes = find_clashes() assert len(clashes) == 2 pos = (venue2, slot2) assert pos in clashes assert item3 in clashes[pos] assert item4 in clashes[pos] # Fix clashes item1.slots.remove(slot2) item3.slots.remove(slot2) item3.slots.add(slot1) clashes = find_clashes() assert len(clashes) == 0 def test_validation(self): """Test that we detect validation errors correctly""" # Create a item with both a talk and a page assigned day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) end = D.time(12, 0, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start1, end_time=end, day=day1) user = get_user_model().objects.create_user('john', 'best@wafer.test', 'johnpassword') talk = Talk.objects.create(title="Test talk", status=ACCEPTED, corresponding_author_id=user.id) page = Page.objects.create(name="test page", slug="test") item1 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk, page_id=page.pk) item1.slots.add(slot1) invalid = validate_items() assert set(invalid) == set([item1]) item2 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) item2.slots.add(slot2) # Test talk status talk.status = REJECTED talk.save() invalid = validate_items() assert set(invalid) == set([item1, item2]) talk.status = PENDING talk.save() invalid = validate_items() assert set(invalid) == set([item1, item2]) talk.status = ACCEPTED talk.save() invalid = validate_items() assert set(invalid) == set([item1]) item3 = ScheduleItem.objects.create(venue=venue1, talk_id=None, page_id=None) item3.slots.add(slot2) invalid = validate_items() assert set(invalid) == set([item1, item3]) def test_non_contiguous(self): """Test that we detect items with non contiguous slots""" # Create a item with a gap in the slots assigned to it day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) end = D.time(13, 0, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=start3, day=day1) slot3 = Slot.objects.create(start_time=start3, end_time=end, day=day1) user = get_user_model().objects.create_user('john', 'best@wafer.test', 'johnpassword') talk = Talk.objects.create(title="Test talk", status=ACCEPTED, corresponding_author_id=user.id) page = Page.objects.create(name="test page", slug="test") item1 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) item1.slots.add(slot1) item1.slots.add(slot3) item2 = ScheduleItem.objects.create(venue=venue1, page_id=page.pk) item2.slots.add(slot2) invalid = find_non_contiguous() # Only item1 is invalid assert set(invalid) == set([item1]) item1.slots.add(slot2) item1.slots.remove(slot1) item2.slots.add(slot1) item2.slots.remove(slot2) invalid = validate_items() # Everything is valid now assert set(invalid) == set([]) def test_duplicates(self): """Test that we can detect duplicates talks and pages""" # Final chedule is # Venue 1 Venue 2 # 10-11 Talk 1 Page 1 # 11-12 Talk 1 Page 1 day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue2 = Venue.objects.create(order=2, name='Venue 2') venue1.days.add(day1) venue2.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) end = D.time(12, 0, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start1, end_time=end, day=day1) user = get_user_model().objects.create_user('john', 'best@wafer.test', 'johnpassword') talk = Talk.objects.create(title="Test talk", status=ACCEPTED, corresponding_author_id=user.id) page1 = Page.objects.create(name="test page", slug="test") page2 = Page.objects.create(name="test page 2", slug="test2") item1 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) item1.slots.add(slot1) item2 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) item2.slots.add(slot2) duplicates = find_duplicate_schedule_items() assert set(duplicates) == set([item1, item2]) item3 = ScheduleItem.objects.create(venue=venue2, page_id=page1.pk) item3.slots.add(slot1) item4 = ScheduleItem.objects.create(venue=venue2, talk_id=talk.pk) item4.slots.add(slot2) duplicates = find_duplicate_schedule_items() assert set(duplicates) == set([item1, item2, item4]) item4.page_id = page2.pk item4.talk_id = None item4.save() duplicates = find_duplicate_schedule_items() assert set(duplicates) == set([item1, item2]) def test_venues(self): """Test that we detect venues violating the day constraints correctly.""" day1 = Day.objects.create(date=D.date(2013, 9, 22)) day2 = Day.objects.create(date=D.date(2013, 9, 23)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue2 = Venue.objects.create(order=2, name='Venue 2') venue1.days.add(day1) venue2.days.add(day2) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) page = Page.objects.create(name="test page", slug="test") item1 = ScheduleItem.objects.create(venue=venue1, page_id=page.pk) item1.slots.add(slot1) item2 = ScheduleItem.objects.create(venue=venue2, page_id=page.pk) item2.slots.add(slot1) venues = find_invalid_venues() assert set(venues) == set([venue2]) assert set(venues[venue2]) == set([item2]) slot2 = Slot.objects.create(start_time=start1, end_time=start2, day=day2) item3 = ScheduleItem.objects.create(venue=venue2, page_id=page.pk) item3.slots.add(slot2) venues = find_invalid_venues() assert set(venues) == set([venue2]) assert set(venues[venue2]) == set([item2]) item4 = ScheduleItem.objects.create(venue=venue1, page_id=page.pk) item5 = ScheduleItem.objects.create(venue=venue2, page_id=page.pk) item4.slots.add(slot2) item5.slots.add(slot1) venues = find_invalid_venues() assert set(venues) == set([venue1, venue2]) assert set(venues[venue1]) == set([item4]) assert set(venues[venue2]) == set([item2, item5]) PKH1!"wafer/schedule/tests/test_views.pyimport json import datetime as D from django.test import Client, TestCase from django.contrib.auth import get_user_model from wafer.talks.models import Talk, ACCEPTED from wafer.pages.models import Page from wafer.schedule.models import Day, Venue, Slot, ScheduleItem from wafer.utils import QueryTracker def make_pages(n): """ Make n pages. """ pages = [] for x in range(n): page = Page.objects.create(name="test page %s" % x, slug="test%s" % x) pages.append(page) return pages def make_items(venues, pages, expand=()): """ Make items for pairs of venues and pages. """ items = [] for x, (venue, page) in enumerate(zip(venues, pages)): item = ScheduleItem.objects.create(venue=venue, details="Item %s" % x, page_id=page.pk, expand=x in expand) items.append(item) return items def make_venue(order=1, name='Venue 1'): """ Make a venue. """ venue = Venue.objects.create(order=1, name='Venue 1') return venue def make_slot(): """ Make a slot. """ day = Day.objects.create(date=D.date(2013, 9, 22)) start = D.time(10, 0, 0) end = D.time(15, 0, 0) slot = Slot.objects.create(start_time=start, end_time=end, day=day) return slot def create_client(username=None, superuser=False): client = Client() if username: email = '%s@example.com' % (username,) password = '%s_password' % (username,) if superuser: create = get_user_model().objects.create_superuser else: create = get_user_model().objects.create_user create(username, email, password) client.login(username=username, password=password) return client class ScheduleViewTests(TestCase): def test_simple_table(self): """Create a simple, single day table with 3 slots and 2 venues and check we get the expected results""" # Schedule is # Venue 1 Venue 2 # 10-11 Item1 Item4 # 11-12 Item2 Item5 # 12-13 Item3 Item6 day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) venue2 = Venue.objects.create(order=2, name='Venue 2') venue2.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) end = D.time(13, 0, 0) pages = make_pages(6) venues = [venue1, venue1, venue1, venue2, venue2, venue2] items = make_items(venues, pages) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(previous_slot=slot1, end_time=start3, day=day1) slot3 = Slot.objects.create(previous_slot=slot2, end_time=end, day=day1) items[0].slots.add(slot1) items[3].slots.add(slot1) items[1].slots.add(slot2) items[4].slots.add(slot2) items[2].slots.add(slot3) items[5].slots.add(slot3) c = Client() with QueryTracker() as tracker: response = c.get('/schedule/') self.assertTrue(len(tracker.queries) < 60) [day1] = response.context['schedule_days'] assert len(day1.rows) == 3 assert day1.venues == [venue1, venue2] assert day1.rows[0].slot.get_start_time() == start1 assert day1.rows[0].slot.end_time == start2 assert day1.rows[1].slot.get_start_time() == start2 assert day1.rows[1].slot.end_time == start3 assert day1.rows[2].slot.get_start_time() == start3 assert day1.rows[2].slot.end_time == end assert len(day1.rows[0].items) == 2 assert len(day1.rows[1].items) == 2 assert len(day1.rows[2].items) == 2 assert day1.rows[0].get_sorted_items()[0]['item'] == items[0] assert day1.rows[0].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['item'] == items[3] assert day1.rows[0].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['colspan'] == 1 assert day1.rows[1].get_sorted_items()[0]['item'] == items[1] assert day1.rows[1].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[1].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[2].get_sorted_items()[1]['item'] == items[5] assert day1.rows[2].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[2].get_sorted_items()[1]['colspan'] == 1 def test_ordering(self): """Ensure we handle oddly ordered creation of items correctly""" # Schedule is # Venue 1 Venue 2 # 10-11 Item3 Item6 # 11-12 Item2 Item5 # 12-13 Item1 Item4 day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) venue2 = Venue.objects.create(order=2, name='Venue 2') venue2.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) end = D.time(13, 0, 0) pages = make_pages(6) venues = [venue1, venue1, venue1, venue2, venue2, venue2] items = make_items(venues, pages) # Create the slots not in date order either slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot3 = Slot.objects.create(previous_slot=slot1, end_time=end, day=day1) slot2 = Slot.objects.create(previous_slot=slot1, end_time=start3, day=day1) slot3.previous_slot = slot2 slot3.save() items[0].slots.add(slot3) items[3].slots.add(slot3) items[1].slots.add(slot2) items[4].slots.add(slot2) items[2].slots.add(slot1) items[5].slots.add(slot1) c = Client() response = c.get('/schedule/') [day1] = response.context['schedule_days'] assert len(day1.rows) == 3 assert day1.venues == [venue1, venue2] assert day1.rows[0].slot.get_start_time() == start1 assert day1.rows[0].slot.end_time == start2 assert day1.rows[1].slot.get_start_time() == start2 assert day1.rows[1].slot.end_time == start3 assert day1.rows[2].slot.get_start_time() == start3 assert day1.rows[2].slot.end_time == end assert len(day1.rows[0].items) == 2 assert len(day1.rows[1].items) == 2 assert len(day1.rows[2].items) == 2 assert day1.rows[0].get_sorted_items()[0]['item'] == items[2] assert day1.rows[0].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['item'] == items[5] assert day1.rows[0].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['colspan'] == 1 assert day1.rows[1].get_sorted_items()[0]['item'] == items[1] assert day1.rows[1].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[1].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[2].get_sorted_items()[1]['item'] == items[3] assert day1.rows[2].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[2].get_sorted_items()[1]['colspan'] == 1 def test_multiple_days(self): """Create a multiple day table with 3 slots and 2 venues and check we get the expected results""" # Schedule is # Venue 1 Venue 2 # Day1 # 10-11 Item1 Item4 # 11-12 Item2 Item5 # Day2 # 12-13 Item3 Item6 day1 = Day.objects.create(date=D.date(2013, 9, 22)) day2 = Day.objects.create(date=D.date(2013, 9, 23)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) venue1.days.add(day2) venue2 = Venue.objects.create(order=2, name='Venue 2') venue2.days.add(day1) venue2.days.add(day2) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) end1 = D.time(12, 0, 0) start3 = D.time(12, 0, 0) end2 = D.time(13, 0, 0) pages = make_pages(6) venues = [venue1, venue1, venue1, venue2, venue2, venue2] items = make_items(venues, pages) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=end1, day=day1) slot3 = Slot.objects.create(start_time=start3, end_time=end2, day=day2) items[0].slots.add(slot1) items[3].slots.add(slot1) items[1].slots.add(slot2) items[4].slots.add(slot2) items[2].slots.add(slot3) items[5].slots.add(slot3) c = Client() response = c.get('/schedule/') [day1, day2] = response.context['schedule_days'] assert len(day1.rows) == 2 assert day1.venues == [venue1, venue2] assert len(day2.rows) == 1 assert day2.venues == [venue1, venue2] assert day1.rows[0].slot.get_start_time() == start1 assert day1.rows[0].slot.end_time == start2 assert day1.rows[1].slot.get_start_time() == start2 assert day1.rows[1].slot.end_time == end1 assert day2.rows[0].slot.get_start_time() == start3 assert day2.rows[0].slot.end_time == end2 assert len(day1.rows[0].items) == 2 assert len(day1.rows[1].items) == 2 assert len(day2.rows[0].items) == 2 assert day1.rows[0].get_sorted_items()[0]['item'] == items[0] assert day1.rows[0].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[0]['colspan'] == 1 assert day2.rows[0].get_sorted_items()[1]['item'] == items[5] assert day2.rows[0].get_sorted_items()[1]['rowspan'] == 1 assert day2.rows[0].get_sorted_items()[1]['colspan'] == 1 def test_per_day_view(self): """Create a multiple day table with 3 slots and 2 venues and check we get the expected results using the per-day views""" # This is the same schedule as test_multiple_days day1 = Day.objects.create(date=D.date(2013, 9, 22)) day2 = Day.objects.create(date=D.date(2013, 9, 23)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) venue1.days.add(day2) venue2 = Venue.objects.create(order=2, name='Venue 2') venue2.days.add(day1) venue2.days.add(day2) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) end1 = D.time(12, 0, 0) start3 = D.time(12, 0, 0) end2 = D.time(13, 0, 0) pages = make_pages(6) venues = [venue1, venue1, venue1, venue2, venue2, venue2] items = make_items(venues, pages) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=end1, day=day1) slot3 = Slot.objects.create(start_time=start3, end_time=end2, day=day2) items[0].slots.add(slot1) items[3].slots.add(slot1) items[1].slots.add(slot2) items[4].slots.add(slot2) items[2].slots.add(slot3) items[5].slots.add(slot3) c = Client() # Check that a wrong day gives the full schedule response = c.get('/schedule/?day=2013-09-24') [day1, day2] = response.context['schedule_days'] self.assertEqual(len(day1.rows), 2) self.assertEqual(day1.venues, [venue1, venue2]) self.assertEqual(len(day2.rows), 1) self.assertEqual(day2.venues, [venue1, venue2]) self.assertEqual(day1.rows[0].slot.get_start_time(), start1) self.assertEqual(day1.rows[0].slot.end_time, start2) self.assertEqual(day1.rows[1].slot.get_start_time(), start2) self.assertEqual(day1.rows[1].slot.end_time, end1) self.assertEqual(day2.rows[0].slot.get_start_time(), start3) self.assertEqual(day2.rows[0].slot.end_time, end2) self.assertEqual(len(day1.rows[0].items), 2) self.assertEqual(len(day1.rows[1].items), 2) self.assertEqual(len(day2.rows[0].items), 2) self.assertEqual(day1.rows[0].get_sorted_items()[0]['item'], items[0]) self.assertEqual(day1.rows[0].get_sorted_items()[0]['rowspan'], 1) self.assertEqual(day1.rows[0].get_sorted_items()[0]['colspan'], 1) self.assertEqual(day2.rows[0].get_sorted_items()[1]['item'], items[5]) self.assertEqual(day2.rows[0].get_sorted_items()[1]['rowspan'], 1) self.assertEqual(day2.rows[0].get_sorted_items()[1]['colspan'], 1) # Test per-day schedule views response = c.get('/schedule/?day=2013-09-22') [day] = response.context['schedule_days'] self.assertEqual(day.day, day1.day) self.assertEqual(day.venues, day1.venues) self.assertEqual(len(day.rows), len(day1.rows)) # Repeat a bunch of tests from the full schedule self.assertEqual(day.rows[0].slot.get_start_time(), start1) self.assertEqual(day.rows[0].slot.end_time, start2) self.assertEqual(len(day.rows[0].items), 2) self.assertEqual(len(day.rows[1].items), 2) self.assertEqual(day.rows[0].get_sorted_items()[0]['item'], items[0]) self.assertEqual(day.rows[0].get_sorted_items()[0]['rowspan'], 1) self.assertEqual(day.rows[0].get_sorted_items()[0]['colspan'], 1) response = c.get('/schedule/?day=2013-09-23') [day] = response.context['schedule_days'] self.assertEqual(day.day, day2.day) self.assertEqual(day.venues, day2.venues) self.assertEqual(len(day.rows), len(day2.rows)) # Repeat a bunch of tests from the full schedule self.assertEqual(day.rows[0].slot.get_start_time(), start3) self.assertEqual(day.rows[0].slot.end_time, end2) self.assertEqual(len(day.rows[0].items), 2) self.assertEqual(day.rows[0].get_sorted_items()[1]['item'], items[5]) self.assertEqual(day.rows[0].get_sorted_items()[1]['rowspan'], 1) self.assertEqual(day.rows[0].get_sorted_items()[1]['colspan'], 1) def test_multiple_days_with_disjoint_venues(self): """Create a multiple day table with 3 slots and 2 venues and check we get the expected results""" # Schedule is # Day1 # Venue 1 # 10-11 Item1 # 11-12 Item2 # Day2 # Venue 2 # 12-13 Item3 day1 = Day.objects.create(date=D.date(2013, 9, 22)) day2 = Day.objects.create(date=D.date(2013, 9, 23)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) venue2 = Venue.objects.create(order=2, name='Venue 2') venue2.days.add(day2) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) end1 = D.time(12, 0, 0) start3 = D.time(12, 0, 0) end2 = D.time(13, 0, 0) pages = make_pages(3) venues = [venue1, venue1, venue2] items = make_items(venues, pages) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=end1, day=day1) slot3 = Slot.objects.create(start_time=start3, end_time=end2, day=day2) items[0].slots.add(slot1) items[1].slots.add(slot2) items[2].slots.add(slot3) c = Client() response = c.get('/schedule/') [day1, day2] = response.context['schedule_days'] assert len(day1.rows) == 2 assert day1.venues == [venue1] assert len(day2.rows) == 1 assert day2.venues == [venue2] assert day1.rows[0].slot.get_start_time() == start1 assert day1.rows[0].slot.end_time == start2 assert day1.rows[1].slot.get_start_time() == start2 assert day1.rows[1].slot.end_time == end1 assert day2.rows[0].slot.get_start_time() == start3 assert day2.rows[0].slot.end_time == end2 assert len(day1.rows[0].items) == 1 assert len(day1.rows[1].items) == 1 assert len(day2.rows[0].items) == 1 assert day1.rows[0].get_sorted_items()[0]['item'] == items[0] assert day1.rows[0].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[0]['colspan'] == 1 assert day2.rows[0].get_sorted_items()[0]['item'] == items[2] assert day2.rows[0].get_sorted_items()[0]['rowspan'] == 1 assert day2.rows[0].get_sorted_items()[0]['colspan'] == 1 def test_col_span(self): """Create table with 3 venues and some interesting venue spanning items""" # Schedule is # Venue 1 Venue 2 Venue3 # 10-11 Item0 -- Item6 + # 11-12 Item1 + -- Item7 + # 12-13 Item2 -- Item8 # 13-14 -- Item4 + -- # 14-15 Item3 + Item5 -- day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue2 = Venue.objects.create(order=2, name='Venue 2') venue3 = Venue.objects.create(order=3, name='Venue 3') venue1.days.add(day1) venue2.days.add(day1) venue3.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) start4 = D.time(13, 0, 0) start5 = D.time(14, 0, 0) end = D.time(15, 0, 0) # We create the slots out of order to tt slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot4 = Slot.objects.create(start_time=start4, end_time=start5, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=start3, day=day1) slot3 = Slot.objects.create(start_time=start3, end_time=start4, day=day1) slot5 = Slot.objects.create(start_time=start5, end_time=end, day=day1) pages = make_pages(10) venues = [venue1, venue1, venue1, venue1, venue2, venue2, venue3, venue3, venue3] expand = [1, 3, 4, 6, 7] items = make_items(venues, pages, expand) items[0].slots.add(slot1) items[6].slots.add(slot1) items[1].slots.add(slot2) items[7].slots.add(slot2) items[2].slots.add(slot3) items[8].slots.add(slot3) items[4].slots.add(slot4) items[3].slots.add(slot5) items[5].slots.add(slot5) c = Client() response = c.get('/schedule/') [day1] = response.context['schedule_days'] assert len(day1.rows) == 5 assert day1.venues == [venue1, venue2, venue3] assert day1.rows[0].slot.get_start_time() == start1 assert day1.rows[1].slot.get_start_time() == start2 assert day1.rows[2].slot.get_start_time() == start3 assert day1.rows[3].slot.get_start_time() == start4 assert day1.rows[4].slot.get_start_time() == start5 assert len(day1.rows[0].items) == 2 assert day1.rows[0].get_sorted_items()[0]['item'] == items[0] assert day1.rows[0].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['item'] == items[6] assert day1.rows[0].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['colspan'] == 2 assert len(day1.rows[1].items) == 2 assert day1.rows[1].get_sorted_items()[0]['item'] == items[1] assert day1.rows[1].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[1].get_sorted_items()[0]['colspan'] == 2 assert day1.rows[1].get_sorted_items()[1]['item'] == items[7] assert day1.rows[1].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[1].get_sorted_items()[1]['colspan'] == 1 assert len(day1.rows[2].items) == 3 assert day1.rows[2].get_sorted_items()[0]['item'] == items[2] assert day1.rows[2].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[2].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[2].get_sorted_items()[1]['item'] is None assert day1.rows[2].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[2].get_sorted_items()[1]['colspan'] == 1 assert day1.rows[2].get_sorted_items()[2]['item'] == items[8] assert day1.rows[2].get_sorted_items()[2]['rowspan'] == 1 assert day1.rows[2].get_sorted_items()[2]['colspan'] == 1 assert len(day1.rows[3].items) == 1 assert day1.rows[3].get_sorted_items()[0]['item'] == items[4] assert day1.rows[3].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[3].get_sorted_items()[0]['colspan'] == 3 assert len(day1.rows[4].items) == 3 assert day1.rows[4].get_sorted_items()[0]['item'] == items[3] assert day1.rows[4].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[4].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[4].get_sorted_items()[1]['item'] == items[5] assert day1.rows[4].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[4].get_sorted_items()[1]['colspan'] == 1 assert day1.rows[4].get_sorted_items()[2]['item'] is None assert day1.rows[4].get_sorted_items()[2]['rowspan'] == 1 assert day1.rows[4].get_sorted_items()[1]['colspan'] == 1 def test_row_span(self): """Create a day table with multiple slot items""" # Schedule is # Venue 1 Venue 2 # 10-11 Item1 Item5 # 11-12 | Item6 # 12-13 Item2 Item7 # 13-14 Item3 | # 14-15 Item4 | day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue2 = Venue.objects.create(order=2, name='Venue 2') venue1.days.add(day1) venue2.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) start4 = D.time(13, 0, 0) start5 = D.time(14, 0, 0) end = D.time(15, 0, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=start3, day=day1) slot3 = Slot.objects.create(start_time=start3, end_time=start4, day=day1) slot4 = Slot.objects.create(start_time=start4, end_time=start5, day=day1) slot5 = Slot.objects.create(start_time=start5, end_time=end, day=day1) pages = make_pages(7) venues = [venue1, venue1, venue1, venue1, venue2, venue2, venue2] items = make_items(venues, pages) items[0].slots.add(slot1) items[0].slots.add(slot2) items[4].slots.add(slot1) items[5].slots.add(slot2) items[6].slots.add(slot3) items[6].slots.add(slot4) items[6].slots.add(slot5) items[1].slots.add(slot3) items[2].slots.add(slot4) items[3].slots.add(slot5) c = Client() response = c.get('/schedule/') [day1] = response.context['schedule_days'] assert len(day1.rows) == 5 assert day1.venues == [venue1, venue2] assert day1.rows[0].slot.get_start_time() == start1 assert day1.rows[1].slot.get_start_time() == start2 assert day1.rows[4].slot.end_time == end assert len(day1.rows[0].items) == 2 assert len(day1.rows[1].items) == 1 assert len(day1.rows[2].items) == 2 assert len(day1.rows[3].items) == 1 assert len(day1.rows[4].items) == 1 assert day1.rows[0].get_sorted_items()[0]['item'] == items[0] assert day1.rows[0].get_sorted_items()[0]['rowspan'] == 2 assert day1.rows[0].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['item'] == items[4] assert day1.rows[0].get_sorted_items()[1]['rowspan'] == 1 assert day1.rows[0].get_sorted_items()[1]['colspan'] == 1 assert day1.rows[1].get_sorted_items()[0]['item'] == items[5] assert day1.rows[1].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[1].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[2].get_sorted_items()[0]['item'] == items[1] assert day1.rows[2].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[2].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[2].get_sorted_items()[1]['item'] == items[6] assert day1.rows[2].get_sorted_items()[1]['rowspan'] == 3 assert day1.rows[2].get_sorted_items()[1]['colspan'] == 1 assert day1.rows[3].get_sorted_items()[0]['item'] == items[2] assert day1.rows[3].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[3].get_sorted_items()[0]['colspan'] == 1 assert day1.rows[4].get_sorted_items()[0]['item'] == items[3] assert day1.rows[4].get_sorted_items()[0]['rowspan'] == 1 assert day1.rows[4].get_sorted_items()[0]['colspan'] == 1 class CurrentViewTests(TestCase): def test_current_view_simple(self): """Create a schedule and check that the current view looks sane.""" day1 = Day.objects.create(date=D.date(2013, 9, 22)) day2 = Day.objects.create(date=D.date(2013, 9, 23)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue2 = Venue.objects.create(order=2, name='Venue 2') venue1.days.add(day1) venue2.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) start4 = D.time(13, 0, 0) start5 = D.time(14, 0, 0) # During the first slot cur1 = D.time(10, 30, 0) # Middle of the day cur2 = D.time(11, 30, 0) cur3 = D.time(12, 30, 0) # During the last slot cur4 = D.time(13, 30, 0) # After the last slot cur5 = D.time(15, 30, 0) slots = [] slots.append(Slot.objects.create(start_time=start1, end_time=start2, day=day1)) slots.append(Slot.objects.create(start_time=start2, end_time=start3, day=day1)) slots.append(Slot.objects.create(start_time=start3, end_time=start4, day=day1)) slots.append(Slot.objects.create(start_time=start4, end_time=start5, day=day1)) pages = make_pages(8) venues = [venue1, venue2] * 4 items = make_items(venues, pages) for index, item in enumerate(items): item.slots.add(slots[index // 2]) c = Client() response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur1.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slots[0] assert len(context['schedule_day'].venues) == 2 # Only cur and next slot assert len(context['slots']) == 2 assert context['slots'][0].items[venue1]['note'] == 'current' assert context['slots'][1].items[venue1]['note'] == 'forthcoming' response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur2.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slots[1] assert len(context['schedule_day'].venues) == 2 # prev, cur and next slot assert len(context['slots']) == 3 assert context['slots'][0].items[venue1]['note'] == 'complete' assert context['slots'][1].items[venue1]['note'] == 'current' assert context['slots'][2].items[venue1]['note'] == 'forthcoming' response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur3.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slots[2] assert len(context['schedule_day'].venues) == 2 # prev and cur assert len(context['slots']) == 3 assert context['slots'][0].items[venue1]['note'] == 'complete' assert context['slots'][1].items[venue1]['note'] == 'current' assert context['slots'][2].items[venue1]['note'] == 'forthcoming' response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur4.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slots[3] assert len(context['schedule_day'].venues) == 2 # preve and cur slot assert len(context['slots']) == 2 assert context['slots'][0].items[venue1]['note'] == 'complete' assert context['slots'][1].items[venue1]['note'] == 'current' response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur5.strftime('%H:%M')}) context = response.context assert context['cur_slot'] is None assert len(context['schedule_day'].venues) == 2 # prev slot only assert len(context['slots']) == 1 assert context['slots'][0].items[venue1]['note'] == 'complete' # Check that next day is an empty current view response = c.get('/schedule/current/', {'day': day2.date.strftime('%Y-%m-%d'), 'time': cur3.strftime('%H:%M')}) assert len(response.context['slots']) == 0 def test_current_view_complex(self): """Create a schedule with overlapping venues and slotes and check that the current view looks sane.""" # Schedule is # Venue 1 Venue 2 Venue3 # 10-11 Item1 -- Item5 # 11-12 Item2 Item3 Item4 # 12-13 | Item7 Item6 # 13-14 Item8 | | # 14-15 -- Item9 Item10 day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue2 = Venue.objects.create(order=2, name='Venue 2') venue3 = Venue.objects.create(order=3, name='Venue 3') venue1.days.add(day1) venue2.days.add(day1) venue3.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) start3 = D.time(12, 0, 0) start4 = D.time(13, 0, 0) start5 = D.time(14, 0, 0) end = D.time(15, 0, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start2, end_time=start3, day=day1) slot3 = Slot.objects.create(start_time=start3, end_time=start4, day=day1) slot4 = Slot.objects.create(start_time=start4, end_time=start5, day=day1) slot5 = Slot.objects.create(start_time=start5, end_time=end, day=day1) pages = make_pages(10) venues = [venue1, venue1, venue2, venue3, venue3, venue3, venue2, venue1, venue2, venue3] items = make_items(venues, pages) items[0].slots.add(slot1) items[4].slots.add(slot1) items[1].slots.add(slot2) items[1].slots.add(slot3) items[2].slots.add(slot2) items[3].slots.add(slot2) items[5].slots.add(slot3) items[5].slots.add(slot4) items[6].slots.add(slot3) items[6].slots.add(slot4) items[7].slots.add(slot4) items[8].slots.add(slot5) items[9].slots.add(slot5) # During the first slot cur1 = D.time(10, 30, 0) # Middle of the day cur2 = D.time(11, 30, 0) cur3 = D.time(12, 30, 0) # During the last slot cur4 = D.time(14, 30, 0) c = Client() response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur1.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slot1 assert len(context['schedule_day'].venues) == 3 assert len(context['slots']) == 2 assert context['slots'][0].items[venue1]['note'] == 'current' assert context['slots'][0].items[venue1]['colspan'] == 1 assert context['slots'][0].items[venue2]['item'] is None response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur2.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slot2 assert len(context['slots']) == 3 assert context['slots'][0].items[venue1]['note'] == 'complete' assert context['slots'][1].items[venue1]['note'] == 'current' assert context['slots'][1].items[venue1]['rowspan'] == 2 assert context['slots'][1].items[venue2]['note'] == 'current' assert context['slots'][1].items[venue2]['rowspan'] == 1 # We truncate the rowspan for this event assert context['slots'][2].items[venue2]['note'] == 'forthcoming' assert context['slots'][2].items[venue2]['rowspan'] == 1 response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur3.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slot3 assert len(context['slots']) == 3 # This event is still current, even though it started last slot assert context['slots'][0].items[venue1]['note'] == 'current' assert context['slots'][0].items[venue1]['rowspan'] == 2 # Venue 2 now has rowspan 2 assert context['slots'][1].items[venue2]['note'] == 'current' assert context['slots'][1].items[venue2]['rowspan'] == 2 response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur4.strftime('%H:%M')}) context = response.context assert context['cur_slot'] == slot5 assert len(context['slots']) == 2 assert context['slots'][0].items[venue1]['note'] == 'complete' assert context['slots'][0].items[venue1]['rowspan'] == 1 # Items are truncated to 1 row assert context['slots'][0].items[venue2]['note'] == 'complete' assert context['slots'][0].items[venue2]['rowspan'] == 1 def test_current_view_invalid(self): """Test that invalid schedules return a inactive current view.""" day1 = Day.objects.create(date=D.date(2013, 9, 22)) venue1 = Venue.objects.create(order=1, name='Venue 1') venue1.days.add(day1) start1 = D.time(10, 0, 0) start2 = D.time(11, 0, 0) end = D.time(12, 0, 0) cur1 = D.time(10, 30, 0) slot1 = Slot.objects.create(start_time=start1, end_time=start2, day=day1) slot2 = Slot.objects.create(start_time=start1, end_time=end, day=day1) user = get_user_model().objects.create_user('john', 'best@wafer.test', 'johnpassword') talk = Talk.objects.create(title="Test talk", status=ACCEPTED, corresponding_author_id=user.id) item1 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) item1.slots.add(slot1) item2 = ScheduleItem.objects.create(venue=venue1, talk_id=talk.pk) item2.slots.add(slot2) c = Client() response = c.get('/schedule/current/', {'day': day1.date.strftime('%Y-%m-%d'), 'time': cur1.strftime('%H:%M')}) assert response.context['active'] is False class ScheduleItemViewSetTests(TestCase): def test_unauthorized_users_are_forbidden(self): c = create_client('ordinary', superuser=False) response = c.get('/schedule/api/scheduleitems/') self.assertEqual(response.status_code, 403) self.assertEqual(response.data, { "detail": "You do not have permission to perform this action.", }) def test_list_scheduleitems(self): venue = make_venue() [page] = make_pages(1) [item] = make_items([venue], [page]) slot = make_slot() item.slots.add(slot) c = create_client('super', superuser=True) response = c.get('/schedule/api/scheduleitems/') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, { "count": 1, "next": None, "previous": None, "results": [{ "id": item.pk, "venue": venue.pk, "slots": [slot.pk], "page": page.pk, "talk": None, }], }) def test_create_scheduleitem(self): venue = make_venue() slot = make_slot() [page] = make_pages(1) c = create_client('super', superuser=True) response = c.post( '/schedule/api/scheduleitems/', data=json.dumps({ "venue": venue.pk, "slots": [slot.pk], "page": page.pk, "talk": '', }), content_type='application/json') self.assertEqual(response.status_code, 201) [item] = ScheduleItem.objects.all() self.assertEqual(response.data, { 'id': item.pk, 'venue': venue.pk, 'slots': [slot.pk], 'talk': None, 'page': page.pk, }) def test_put_scheduleitem(self): venue = make_venue() slot = make_slot() [page] = make_pages(1) [item] = make_items([venue], [page]) slot = make_slot() c = create_client('super', superuser=True) response = c.put( '/schedule/api/scheduleitems/%s/' % item.pk, data=json.dumps({ "venue": venue.pk, "slots": [slot.pk], "page": page.pk, "talk": '', }), content_type='application/json') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, { 'id': item.pk, 'venue': venue.pk, 'slots': [slot.pk], 'talk': None, 'page': page.pk, }) def test_patch_scheduleitem(self): venue = make_venue() slot = make_slot() [page] = make_pages(1) [item] = make_items([venue], [page]) slot = make_slot() c = create_client('super', superuser=True) response = c.patch( '/schedule/api/scheduleitems/%s/' % item.pk, data=json.dumps({ "slots": [slot.pk], }), content_type='application/json') self.assertEqual(response.status_code, 200) self.assertEqual(response.data, { 'id': item.pk, 'venue': venue.pk, 'slots': [slot.pk], 'talk': None, 'page': page.pk, }) def test_delete_scheduleitem(self): venue = make_venue() [page] = make_pages(1) [item] = make_items([venue], [page]) c = create_client('super', superuser=True) response = c.delete( '/schedule/api/scheduleitems/%s/' % item.pk) self.assertEqual(response.status_code, 204) self.assertEqual(response.data, None) self.assertEqual(ScheduleItem.objects.count(), 0) PK}H:Ewafer/schedule/migrations/0003_protect_scheduleitems_from_deletion.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('schedule', '0002_auto_20140909_1403'), ] operations = [ migrations.AlterField( model_name='scheduleitem', name='venue', field=models.ForeignKey(to='schedule.Venue', on_delete=django.db.models.deletion.PROTECT), ), migrations.AlterField( model_name='slot', name='day', field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, blank=True, to='schedule.Day', help_text='Day for this slot', null=True), ), ] PK}H= vv4wafer/schedule/migrations/0002_auto_20140909_1403.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('schedule', '0001_initial'), ] operations = [ migrations.AlterModelOptions( name='slot', options={'ordering': ['day', 'end_time', 'start_time']}, ), ] PKOIGk$++)wafer/schedule/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations import wafer.snippets.markdown_field class Migration(migrations.Migration): dependencies = [ ('pages', '0001_initial'), ('talks', '0001_initial'), ] operations = [ migrations.CreateModel( name='Day', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('date', models.DateField(null=True, blank=True)), ], options={ 'ordering': ['date'], }, bases=(models.Model,), ), migrations.CreateModel( name='ScheduleItem', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('details', wafer.snippets.markdown_field.MarkdownTextField(help_text='Additional details (if required)', blank=True, add_html_field=False, allow_html=False, html_field_suffix=b'_html')), ('notes', models.TextField(help_text='Notes for the conference organisers', blank=True)), ('css_class', models.CharField(help_text='Custom css class for this schedule item', max_length=128, blank=True)), ('details_html', models.TextField(editable=False)), ('page', models.ForeignKey(blank=True, to='pages.Page', null=True)), ], options={ }, bases=(models.Model,), ), migrations.CreateModel( name='Slot', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('start_time', models.TimeField(help_text='Start time (if no previous slot)', null=True, blank=True)), ('end_time', models.TimeField(help_text='Slot end time', null=True)), ('name', models.CharField(help_text='Identifier for use in the admin panel', max_length=1024, null=True, blank=True)), ('day', models.ForeignKey(blank=True, to='schedule.Day', help_text='Day for this slot', null=True)), ('previous_slot', models.ForeignKey(blank=True, to='schedule.Slot', help_text='Previous slot', null=True)), ], options={ 'ordering': ['end_time', 'start_time'], }, bases=(models.Model,), ), migrations.CreateModel( name='Venue', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('order', models.IntegerField(default=1)), ('name', models.CharField(max_length=1024)), ('notes', wafer.snippets.markdown_field.MarkdownTextField(help_text='Notes or directions that will be useful to conference attendees', blank=True, add_html_field=False, allow_html=False, html_field_suffix=b'_html')), ('notes_html', models.TextField(editable=False)), ('days', models.ManyToManyField(help_text='Days on which this venue will be used.', to='schedule.Day')), ], options={ 'ordering': ['order', 'name'], }, bases=(models.Model,), ), migrations.AlterOrderWithRespectTo( name='slot', order_with_respect_to='day', ), migrations.AddField( model_name='scheduleitem', name='slots', field=models.ManyToManyField(to='schedule.Slot'), preserve_default=True, ), migrations.AddField( model_name='scheduleitem', name='talk', field=models.ForeignKey(blank=True, to='talks.Talk', null=True), preserve_default=True, ), migrations.AddField( model_name='scheduleitem', name='venue', field=models.ForeignKey(to='schedule.Venue'), preserve_default=True, ), ] PKOIG%wafer/schedule/migrations/__init__.pyPKH5wafer/schedule/migrations/0005_scheduleitem_expand.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('schedule', '0004_drop_order_with_respect_to'), ] operations = [ migrations.AddField( model_name='scheduleitem', name='expand', field=models.BooleanField( default=False, help_text='Expand to neighbouring venues'), ), ] PK(Hu!@<wafer/schedule/migrations/0004_drop_order_with_respect_to.py# -*- coding: utf-8 -*- # Generated by Django 1.9.5 on 2016-04-04 15:34 from __future__ import unicode_literals from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('schedule', '0003_protect_scheduleitems_from_deletion'), ] operations = [ migrations.AlterOrderWithRespectTo( name='slot', order_with_respect_to=None, ), ] PK'zIWak%wafer-0.4.0.dist-info/DESCRIPTION.rstwafer ===== |wafer-ci-badge| |wafer-docs-badge| .. |wafer-ci-badge| image:: https://travis-ci.org/CTPUG/wafer.png?branch=master :alt: Travis CI build status :scale: 100% :target: https://travis-ci.org/CTPUG/wafer .. |wafer-docs-badge| image:: https://readthedocs.org/projects/wafer/badge/?version=latest :alt: Wafer documentation :scale: 100% :target: http://wafer.readthedocs.org/ A wafer-thin web application for running small conferences. Built using Django. Licensed under the `ISC License`_. .. _ISC License: https://github.com/CTPUG/wafer/blob/master/LICENSE Documentation ============= Available on `readthedocs.org`_. .. _readthedocs.org: http://wafer.readthedocs.org/ Supported Django versions ========================= Wafer supports Django 1.8 and Django 1.9. Installation ============ 1. ``pip install -r requirements.txt`` should install all the required python and django dependencies. 2. Wafer uses bower to manage javascript dependencies * Install bower in the static files base directory: ``npm install bower`` * Grab the ``bower.json`` file to install the required javascript files ``./node_modules/bower/bin/bower install bower.json`` 3. Install the wafer applications ``manage.py migrate`` 4. If you don't have one yet, create a superuser with ``manage.py createsuperuser``. 5. Examine the ``settings.py`` file and create a ``localsettings.py`` file overriding the defaults as required. ``STATIC_FILES``, ``WAFER_MENUS``, ``MARKITUP_FILTER``, ``WAFER_TALKS_OPEN``, ``WAFER_REGISTRATION_OPEN`` and ``WAFER_PUBLIC_ATTENDEE_LIST`` will probably need to be overridden. If you add extensions to ``MARKITUP_FILTER``, be sure to install the appropriate python packages as well. 6. Log in and configure the Site: * The domain will be used as the base for e-mails sent during registration. * The name will be the conference's name. 7. Wafer uses the Django caching infrastructure in several places, so the cache table needs to be created using ``manage.py createcachetable``. 8. Create the default 'Page Editors' and 'Talk Mentors' groups using ``manage.py wafer_add_default_groups``. 9. Have a fun conference. Installing Bootstrap ==================== The default templates and css files require jquery and bootstrap to work. wafer provides a bower.json file to simplify the installation process. This requires a working nodejs installation. 1. Install bower ``npm install bower`` 2. Use bower to install appropriate versions of bootstrap and jquery ``$(npm bin)/bower install`` 3. Move files to the correct location ``manage.py collectstatic`` Features ======== * Support for adding and editing sponsors via Django admin. * Schedule can be created and updated via Django admin. * Pages for static content, news and so forthe can be handled via Django admin. * Can be delegated to the 'Page Editors' group. * Pages can be updated via the web interface. * Talk submissions and acceptance. * Generate a static version of the site for archival. TODO ==== * Make the code easier to use for other conferences (split out theming, etc). * Improve the talk submission management module: * Better display of accepted talks. * Make various messages easier to customise. * Improve admin support for the schedule: * Show table of slots in admin interface. * Improve handling of moving talks around. * Support for adding news (and other templated pages) via Django admin. * Maybe add some cool visualizations with d3: * Number of people signed up in various categories. * Places remaining. * Sponsorship slots remaining. * Days until various deadlines. * Other improvements * Add many tests PK'zIJ88#wafer-0.4.0.dist-info/metadata.json{"classifiers": ["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: ISC License (ISCL)", "Operating System :: POSIX", "Programming Language :: Python", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Framework :: Django", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Internet :: WWW/HTTP"], "extensions": {"python.details": {"contacts": [{"email": "ctpug@googlegroups.com", "name": "CTPUG", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "http://github.com/CTPUG/wafer"}}}, "extras": [], "generator": "bdist_wheel (0.26.0)", "license": "ISC", "metadata_version": "2.0", "name": "wafer", "run_requires": [{"requires": ["Django (>=1.8)", "backports.csv", "diff-match-patch", "django-crispy-forms", "django-easy-select2", "django-markitup (>=2.2.2)", "django-medusa (>=0.3.0)", "django-nose", "django-registration-redux", "django-reversion (>=2.0)", "djangorestframework", "jsonfield", "markdown (>=2.5)", "pillow", "pyLibravatar", "pydns", "pytz", "requests"]}], "summary": "A wafer-thin Django library for running small conferences.", "version": "0.4.0"}PK&zIY:#wafer-0.4.0.dist-info/top_level.txtwafer PK'zI''\\wafer-0.4.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PK'zI55wafer-0.4.0.dist-info/METADATAMetadata-Version: 2.0 Name: wafer Version: 0.4.0 Summary: A wafer-thin Django library for running small conferences. Home-page: http://github.com/CTPUG/wafer Author: CTPUG Author-email: ctpug@googlegroups.com License: ISC Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: ISC License (ISCL) Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Framework :: Django Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: WWW/HTTP Requires-Dist: Django (>=1.8) Requires-Dist: backports.csv Requires-Dist: diff-match-patch Requires-Dist: django-crispy-forms Requires-Dist: django-easy-select2 Requires-Dist: django-markitup (>=2.2.2) Requires-Dist: django-medusa (>=0.3.0) Requires-Dist: django-nose Requires-Dist: django-registration-redux Requires-Dist: django-reversion (>=2.0) Requires-Dist: djangorestframework Requires-Dist: jsonfield Requires-Dist: markdown (>=2.5) Requires-Dist: pillow Requires-Dist: pyLibravatar Requires-Dist: pydns Requires-Dist: pytz Requires-Dist: requests wafer ===== |wafer-ci-badge| |wafer-docs-badge| .. |wafer-ci-badge| image:: https://travis-ci.org/CTPUG/wafer.png?branch=master :alt: Travis CI build status :scale: 100% :target: https://travis-ci.org/CTPUG/wafer .. |wafer-docs-badge| image:: https://readthedocs.org/projects/wafer/badge/?version=latest :alt: Wafer documentation :scale: 100% :target: http://wafer.readthedocs.org/ A wafer-thin web application for running small conferences. Built using Django. Licensed under the `ISC License`_. .. _ISC License: https://github.com/CTPUG/wafer/blob/master/LICENSE Documentation ============= Available on `readthedocs.org`_. .. _readthedocs.org: http://wafer.readthedocs.org/ Supported Django versions ========================= Wafer supports Django 1.8 and Django 1.9. Installation ============ 1. ``pip install -r requirements.txt`` should install all the required python and django dependencies. 2. Wafer uses bower to manage javascript dependencies * Install bower in the static files base directory: ``npm install bower`` * Grab the ``bower.json`` file to install the required javascript files ``./node_modules/bower/bin/bower install bower.json`` 3. Install the wafer applications ``manage.py migrate`` 4. If you don't have one yet, create a superuser with ``manage.py createsuperuser``. 5. Examine the ``settings.py`` file and create a ``localsettings.py`` file overriding the defaults as required. ``STATIC_FILES``, ``WAFER_MENUS``, ``MARKITUP_FILTER``, ``WAFER_TALKS_OPEN``, ``WAFER_REGISTRATION_OPEN`` and ``WAFER_PUBLIC_ATTENDEE_LIST`` will probably need to be overridden. If you add extensions to ``MARKITUP_FILTER``, be sure to install the appropriate python packages as well. 6. Log in and configure the Site: * The domain will be used as the base for e-mails sent during registration. * The name will be the conference's name. 7. Wafer uses the Django caching infrastructure in several places, so the cache table needs to be created using ``manage.py createcachetable``. 8. Create the default 'Page Editors' and 'Talk Mentors' groups using ``manage.py wafer_add_default_groups``. 9. Have a fun conference. Installing Bootstrap ==================== The default templates and css files require jquery and bootstrap to work. wafer provides a bower.json file to simplify the installation process. This requires a working nodejs installation. 1. Install bower ``npm install bower`` 2. Use bower to install appropriate versions of bootstrap and jquery ``$(npm bin)/bower install`` 3. Move files to the correct location ``manage.py collectstatic`` Features ======== * Support for adding and editing sponsors via Django admin. * Schedule can be created and updated via Django admin. * Pages for static content, news and so forthe can be handled via Django admin. * Can be delegated to the 'Page Editors' group. * Pages can be updated via the web interface. * Talk submissions and acceptance. * Generate a static version of the site for archival. TODO ==== * Make the code easier to use for other conferences (split out theming, etc). * Improve the talk submission management module: * Better display of accepted talks. * Make various messages easier to customise. * Improve admin support for the schedule: * Show table of slots in admin interface. * Improve handling of moving talks around. * Support for adding news (and other templated pages) via Django admin. * Maybe add some cool visualizations with d3: * Number of people signed up in various categories. * Places remaining. * Sponsorship slots remaining. * Days until various deadlines. * Other improvements * Add many tests PK'zIDDwafer-0.4.0.dist-info/RECORDwafer/__init__.py,sha256=moS_KyIn3xPIx1AgTk7lMqnoTSGigH0fla04tiFG5Y0,93 wafer/context_processors.py,sha256=kBK62CHCx6K1F9q9ST1a6lj-39Z_x6y3FAk6A-oJ3Qw,888 wafer/menu.py,sha256=V8WmBfSar35bob-tU0yVo5fer0DjSx1lYAkzI3fTW50,4302 wafer/settings.py,sha256=fR4NQ-zGsWJaCaXuckSwzG-_ZlyQDaop3Bky6OOW6h8,8986 wafer/urls.py,sha256=erNu7d7WHus2OEYwvzr9nG043wRanTygd5i--BhwhmU,945 wafer/utils.py,sha256=OzKBAU2l2B1EboaXgOb9dR6q5bMpChQ3EjDrDoI3xiQ,2034 wafer/wsgi.py,sha256=G9k3_DU5I_QMm2cycxJwPxAbtoeSW6J45Itr1b8RcuQ,1416 wafer/compare/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/compare/admin.py,sha256=boGfYdr-Nl7Gie7Jdj8vzQqLo8v4YwWNP1G_97I0NWg,7245 wafer/compare/templates/admin/wafer.compare/change_form.html,sha256=8khExJDI5tpJh7jBSH_ym2Jnjo1haltf589KWDsER74,628 wafer/compare/templates/admin/wafer.compare/compare.html,sha256=-Tn8dRCGK7LhFsg32T-N_J4W5WPzcIvES9PUAybOJSE,556 wafer/compare/templates/admin/wafer.compare/compare_list.html,sha256=i3n8MGnZH0OHrEuhK_5deTE6JEeLtcsIlBDxgIu8R8k,2515 wafer/kv/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/kv/models.py,sha256=EdEm8mV2tfDzzPYL5j0AsSn90Rg2Y2-xW5BMq0eIuYE,360 wafer/kv/permissions.py,sha256=O8Uc0kjj77X2jyNXOOWCdwm-BKhiFFMIw1CgyotbnSw,1008 wafer/kv/serializers.py,sha256=BlcQne5loSGx4hH0Ut3cqcR0F1KanejN9vjlQGaR9FM,1269 wafer/kv/urls.py,sha256=s8SLNQieJ9SdHjze1ksy3JuyG3NRbhA7-BAI0mSWBVs,282 wafer/kv/utils.py,sha256=HyLlMmZUCDFNt1RadqR2qsRes-twvfECQ29h0q3fbu8,537 wafer/kv/views.py,sha256=c9pgeL_XygaQ0rtdT1qYN4oSKVrYHO4dyC49Wp3CnIg,802 wafer/kv/migrations/0001_initial.py,sha256=Z9DQMLtB8mtunW4an3yuxqqUksYIfKXqvV_pLFMEjqM,740 wafer/kv/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/kv/tests/test_kv_api.py,sha256=JdBzAdug24A2KQO-bu2PvpHxsyTO5d5I4wFqghutUA0,9662 wafer/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/management/static.py,sha256=Q1FumissAtdq-h1tC2WcT37OhuhF1nC7oW6xRocPAuo,1197 wafer/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/management/commands/wafer_add_default_groups.py,sha256=Dx5nGyTmeBEjpC66XoM-1c5XnDxHBJmE5Pzr78dgnDA,1836 wafer/management/commands/wafer_emails.py,sha256=i6hy7XBPQjge9adWi0E_TggcASsXF9nBs49UAEWdPlg,3123 wafer/management/commands/wafer_registered_attendees.py,sha256=9LMdyB-pfjq6tZMlTZ-2upKIovq47KMFYK_wleTQiHU,2895 wafer/management/commands/wafer_speaker_contact_details.py,sha256=vexcHvrfYjPURDuGCbObnYf6bxWq0Q1RyHAzmlDszZs,1517 wafer/management/commands/wafer_speaker_tickets.py,sha256=RYq26AZTrdO2p_4yUa5MWtcbVYdLyGkeU0es_OM6dWE,1660 wafer/management/commands/wafer_stats.py,sha256=4yz_qOsACg4iwFVF8NGPVlFFSMIRTZfw0HGPyaEzU5A,562 wafer/pages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/pages/admin.py,sha256=EAYzzhMLk5zbVmXTIkKPYo6F6NjlKSFY3eTbHrZtWro,483 wafer/pages/forms.py,sha256=o1-Hg5XSuRrYXJBKMUkgqPVibk4zRt1_DW4NUR-pQ7k,564 wafer/pages/models.py,sha256=DAQwRHPkZVaRWXTMBEYUSecW5o9NocqVcO3M_N4Bc3g,4569 wafer/pages/renderers.py,sha256=ET0NUONHF4XcGg1aRN0wYUwtQlUk87H1TD5V-o4Y3tM,608 wafer/pages/serializers.py,sha256=8q41vjwoXoVBhQHwO8F69t6h6Gd1uzh8ky304HQnevQ,1072 wafer/pages/urls.py,sha256=wgOvycSrkNg8lh2GZk6Aq_QHG32GMgmpGBQphniGPJQ,558 wafer/pages/views.py,sha256=JGzpz9IkMCzMEuNPPa874kbFcK7201H4GRRgRAlXWv4,3718 wafer/pages/migrations/0001_initial.py,sha256=-l7vBhj8HiJjUoLjrsyd_9lcRfPrvIgz8pYz4pPvKD0,1918 wafer/pages/migrations/0002_page_people.py,sha256=DRk7JuhyWPUYSBxWQo_7LkUbqBvPNZKYg0ZKzoNSvs4,693 wafer/pages/migrations/0003_non-null_people+files.py,sha256=0TQmG2UuKdSrU7kEmSAFGVXf_DwJQsr_-v3TK8bg-ss,853 wafer/pages/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/pages/templates/wafer.pages/page.html,sha256=ZRN5DHSD1tZvjZ4yESVJExobpNkR5YAJ0Cia4Fd2Zjc,444 wafer/pages/templates/wafer.pages/page_compare.html,sha256=kkcNFyClOI_OkwqhsMJvRn1ZddAV9PXfdjSKagw8xXY,1195 wafer/pages/templates/wafer.pages/page_form.html,sha256=9TMYIpp2DtcHnGH0z_iZDoNYgoWAerDyC8KrOt5Inw8,231 wafer/pages/tests/test_pages.py,sha256=TMCQMv7Ku25cf-6ZUZ8kAoD-8wl4OceCPpex7avW6qU,3855 wafer/registration/__init__.py,sha256=0WgEuJejtPbhSDJj5sinbAXK4tYP88r7qxIC_eQR-g4,66 wafer/registration/apps.py,sha256=2oQ6uPjkefWsbzBR8QfXV1Dcyp05NxpKxPmt62QxW9w,194 wafer/registration/forms.py,sha256=G4mSx5agnAsEXiYc6nB_wVrWU49w_8EQ2gRAkgYFBEE,725 wafer/registration/sso.py,sha256=pBnlBZzhqF44gjAuqap_0_99-e4kwuVFIYjtmE03eCM,6159 wafer/registration/urls.py,sha256=zQ9WrKtvH8C-ZZjLAvgYla17Ofu5-qFNdSiu-byMjUU,1367 wafer/registration/views.py,sha256=WBZK-uAoE2bBlUkNnc8x2ycyIUQ7J5eH0i0bul14lcY,1948 wafer/registration/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/registration/templates/registration/activate.html,sha256=epl-eRYu4-2HZ4XL2z38ccJdO-JX0etT0IWAKN2DrV4,452 wafer/registration/templates/registration/activation_complete.html,sha256=BRra7NHMkt8kEhBNdg4lJpBRrZIq4R-c5y7fI9rWAR4,281 wafer/registration/templates/registration/activation_email.txt,sha256=Z5w1_Mor_HLr07MM9_siA4GvVZykeeuPHaUW7VJiLcc,657 wafer/registration/templates/registration/activation_email_subject.txt,sha256=dzcHvi91ebYrK8NDvSrVlzZH6ke6Nh6bJhT2HpcE0wg,174 wafer/registration/templates/registration/login.html,sha256=Ueomhfe22171cGuMie-7iNk6oWmXGaNeml7-m2YBzD4,1321 wafer/registration/templates/registration/logout.html,sha256=pKA10ruPsdTeQIWheSQsRtvwRjp4OIyCm_kW6t0aT8Y,191 wafer/registration/templates/registration/registration_base.html,sha256=x6BdXTvHSIamM3V9xqPq6H7ZoeIKBm5w5hyqx7DT-vI,32 wafer/registration/templates/registration/registration_complete.html,sha256=gX96r7kW-1Xfuxz4eyGmp0DgbtOZSVWi5c6hEn8mWl4,348 wafer/registration/templates/registration/registration_form.html,sha256=RNFapJBbeDuFp9qSkvQeV20iqhoQCZu--zXcMh-Dhyc,286 wafer/registration/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/registration/templatetags/wafer_crispy.py,sha256=OBrf50mT35R_9VE-FG44A5yMV3o4VN60WiGx0hABVFw,466 wafer/schedule/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/schedule/admin.py,sha256=_qm9jcqA7NcKEGEqxTEzSwPsqIn57oVNRHqb0YB5Sl0,12298 wafer/schedule/models.py,sha256=ndMcXs9iWVKoTAtO6w8gJlRbESi4l4nvGJ3hXZYT7A4,8002 wafer/schedule/renderers.py,sha256=AdtRtr58nF_lTj3FQMs3fi-O5lb61zHNwoxpnQNjZf0,418 wafer/schedule/serializers.py,sha256=EV-Z5ERiIGTFRnhxWvk-kXShz6nkxG6cPHQ_5Sc_XiQ,1477 wafer/schedule/urls.py,sha256=De11_Tx5HNWRGpARbXpyWge9XJaU3VvYmbxw8k_sLmo,674 wafer/schedule/views.py,sha256=kTCiDEvYLgBfYuLuzu6IFpSHjzl2fOHdW5ZOoTkJz3o,10693 wafer/schedule/migrations/0001_initial.py,sha256=JbxO8v_5iIMjU-UOAYAA_cRw1WC2NVQLi8WyMgfcoaY,4139 wafer/schedule/migrations/0002_auto_20140909_1403.py,sha256=kQv07EZBXlw1gPsPDgrIbWwFCjnSQAqf9aZzmc014v8,374 wafer/schedule/migrations/0003_protect_scheduleitems_from_deletion.py,sha256=9zxROeo-Om3mBDUSa2EkQFUZlkUU2fJMAZte6gD2r0k,745 wafer/schedule/migrations/0004_drop_order_with_respect_to.py,sha256=WEtfZiqIZOn9pCYgNsgJ9QTFIe_4ajB7cjRB5UIQgp8,427 wafer/schedule/migrations/0005_scheduleitem_expand.py,sha256=AQj87FZk6_usfK3zjuPCiU8cHSz20Qyt7_evz5VE9Og,494 wafer/schedule/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/schedule/templates/admin/scheduleitem_list.html,sha256=VY0QXHyLPtw-prVo6Ha-tCDnBxgVDVibc5ruMZhEU_s,2170 wafer/schedule/templates/admin/slot_list.html,sha256=d-6__Eb2lauRvn5acD78LPKxt-HFVCMMlCTmuFNfNJo,625 wafer/schedule/templates/wafer.schedule/current.html,sha256=74-dsH_mKY-ueUhdjD3R0VRhUIyI8ByM2tjDmnjFkMk,3069 wafer/schedule/templates/wafer.schedule/edit_schedule.html,sha256=hcQPx28h0lixBWZIF0R2Jei1MtDUAPQUdODVgnMT2nw,6762 wafer/schedule/templates/wafer.schedule/full_schedule.html,sha256=oGHihopNa6iLBKUXV9vELBPOgs8rXNa64ORMSZYA1Qk,2103 wafer/schedule/templates/wafer.schedule/penta_schedule.xml,sha256=HL66rHmtPwsPrTokfKSR1srjeIaR26StPQT9XNq9Jck,6787 wafer/schedule/templates/wafer.schedule/schedule_item.html,sha256=CMuiwEN6sa3S4Dhbyx5bGP6MstuBkfI76i5Z3e_bPks,390 wafer/schedule/templates/wafer.schedule/venue.html,sha256=rlgPZ_ucwrJyAY-yij6ow2lY3KkyjMkUvs4uHW6-XUE,186 wafer/schedule/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/schedule/tests/test_admin.py,sha256=GO0Y37RXEZ-A7y8dVXN8mwlC3k4ojyVJWa-T4LbYCxE,17821 wafer/schedule/tests/test_models.py,sha256=cl8Izq6hMQU-ekd5PkBYUv4Mvqsq71GwvG6WobxCPQQ,463 wafer/schedule/tests/test_views.py,sha256=Qq7akKzXX2RkPn711-JCCg4YygcuMkdsrR1t5Rrnn30,41449 wafer/snippets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/snippets/markdown_field.py,sha256=aCRrsuAEnYi0pd9gUIyADhoAD3TjlJvu3yIuwDqHa9A,2510 wafer/sponsors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/sponsors/admin.py,sha256=HuV85i9uvxEN8sMzKfg5zD6P9XTdh6QLTCyOocc7-AU,416 wafer/sponsors/models.py,sha256=65XMoYTLsrVqHS8YpgWzPLdF0jGPAQI9O_tlgIpgYCY,2826 wafer/sponsors/renderers.py,sha256=3tg6aA_gL9MJBo8SJZsjtX9pWBuDtDrIgINtmiSBN7o,524 wafer/sponsors/serializers.py,sha256=mnBYfNV7kVlVzAl4alIfycypUh1r6y_nWTgNPwwQ7-4,1585 wafer/sponsors/urls.py,sha256=RDHk6n8iSUwCfOtgNJjVptwuPZFCLWFE4vHOS9xQfrY,637 wafer/sponsors/views.py,sha256=47vrJcrBIGGXZhQYHUKC7sZe3CEnaegyF4eSoKmWWyk,1226 wafer/sponsors/migrations/0001_initial.py,sha256=zBxklRgDkwV4TzvHOjpodpFN21rh7dnJHHdchMlOQAQ,3152 wafer/sponsors/migrations/0002_non-null_files.py,sha256=V3r5ScrjSUdmKvJmqlOMlvKP9gKmNsv8RZ-YAiuK5o4,810 wafer/sponsors/migrations/0003_add_ordering_option.py,sha256=Ty5kmv6Rm8VZPKfg5Ov94AP5fEwm3ujs8oOmKJq7f9U,405 wafer/sponsors/migrations/0004_auto_20160813_1328.py,sha256=C8x9ItnxMXM4sR0DNC9tl7XxutH3GlOpypgadE3d2_0,752 wafer/sponsors/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/sponsors/templates/wafer.sponsors/packages.html,sha256=FsErw4-EsWoP0QiZAmC-Pzmqpi5FJsfu6k4K33hsTAc,859 wafer/sponsors/templates/wafer.sponsors/sponsor.html,sha256=fPSTyU-BOl_X7UO2NtjgNasnyoA_mot9crt5zVfEDRA,343 wafer/sponsors/templates/wafer.sponsors/sponsors.html,sha256=avGR4gREqWgI7zalxhy3SCD3WW2rh3Z6gMv-gNrFSU8,892 wafer/sponsors/templates/wafer.sponsors/sponsors_block.html,sha256=xM7-b4l50cGlxruRyBYdkBFSLoGY4eMEOFPMdkom2wY,677 wafer/sponsors/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/sponsors/templatetags/sponsors.py,sha256=nmblyUjHj3Z9Y1g-rPnVFJSP9bvXVc6S726OoW5Y298,365 wafer/static/css/wafer.css,sha256=6EkYTWs3n6wzygUY4cdlDBjcI2S1n2QIaDslqZTWC6g,1865 wafer/static/img/glyphicons-halflings-white.png,sha256=8ODZWpyKvN-r9GNI4tQoWCm7BJH19q8OBa9Sv_tjJMQ,8777 wafer/static/img/glyphicons-halflings.png,sha256=2Z4_oyxkEDLwgUmRSyjC3GrPLsYvcJh_Ilnqu_p_wN4,12799 wafer/static/js/edit_schedule.js,sha256=PoE0CJlNB-9-I8kA35V8iqN-3HECeAjzj-soBFpcifA,6906 wafer/static/js/scheduledatetime.js,sha256=wF5kZsodI26w6_zJP96VLHVITxHA22nXOd8tc2zBxi4,1156 wafer/talks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/talks/admin.py,sha256=P9te8N62pXKMcvIXJl0U4HeBX6X32yxOKHJreTeATZY,2134 wafer/talks/forms.py,sha256=8EI-lZDR34gEpTMOfhTCudBvqo9F2WG53jQmJTULZyc,2940 wafer/talks/models.py,sha256=-qaPbxWLjgImXhVlFl0MCC9yMFUAKb7RPXDJ3QDBJOM,6403 wafer/talks/renderers.py,sha256=vLCsu9WmKljrPRzJyIvOORpXVAq-dumOk50TFKNRPXA,841 wafer/talks/serializers.py,sha256=ycP4qJNaT1Ujgt6JvKYdlgnu-jSck1Fl-WsjxAD83o8,1444 wafer/talks/urls.py,sha256=oiXy_nR3nXKTByls6Wc34-l38JnPhqhAgwlcWdJOOgQ,906 wafer/talks/views.py,sha256=vW57ycaIEmAYf6-VxwNBvBLvfWuNOry43qK0gfV1gw0,6386 wafer/talks/migrations/0001_initial.py,sha256=bpmow1xk7NeVtBV85cqkDGL0zBlZJNnyE1nagylIcSI,2588 wafer/talks/migrations/0002_auto_20150813_2327.py,sha256=TyTNXcAeNW_6aSXU8xJ5TlqTb1E-jglE98bIGQ57zJY,383 wafer/talks/migrations/0003_talk_private_notes.py,sha256=DH6H9bfJ4Oo4mxhhybawepTU3AqUQt04xjqPOwbUmW0,527 wafer/talks/migrations/0004_edit_private_notes_permission.py,sha256=bdvDnyzXE7xEI0dPIs5NZYSte-Yk_PMbLNOxy87DD0Y,454 wafer/talks/migrations/0005_add_kv.py,sha256=gtfS_Mft_AyJkm-cAZkPWiz81icKMnJaE6YDV07EjUI,441 wafer/talks/migrations/0006_author_helptext.py,sha256=huU_sPQ3AqjN_Tnt7LG_0IN2Hsz1hX9pRW7ervEL1Ek,826 wafer/talks/migrations/0007_add_ordering_option.py,sha256=S8vqHXS4BG06N14k2YK2W9ztw51Syt_KPK2n3BNA2aA,404 wafer/talks/migrations/0008_auto_20160629_1404.py,sha256=eoSRdly5vBMyd8hFZxACvuBtFw5mBYEbD4LkLYjNWYo,527 wafer/talks/migrations/0009_auto_20160813_1819.py,sha256=ufAYnmekKt3Mw-tWrmvrsmTCGlfVQjSXqJOn-CAyL9I,757 wafer/talks/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/talks/templates/wafer.talks/speakers.html,sha256=yDwYOst2Qnq4dGTO9feU2zfFUXH7H6xC3k6ONtxOOGk,865 wafer/talks/templates/wafer.talks/talk.html,sha256=4DWahwd2Av_H4J3WvmZTgnVIAugm2wS0sRjsr511bqg,2793 wafer/talks/templates/wafer.talks/talk_delete.html,sha256=Iv1DUTjUdvz0f_Itii9cCV6y1i5er2gsKUDEdFn247Y,622 wafer/talks/templates/wafer.talks/talk_form.html,sha256=Vub1htHOcgvIZLGqnbinAT1cP6EiBbICIRqkZdFBtaw,791 wafer/talks/templates/wafer.talks/talks.html,sha256=UOWbM2Vjwl3EHhPIky9BaxyU3uTZddDtHDCwRWL3YmM,1661 wafer/talks/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/talks/tests/test_views.py,sha256=rrHCziYpxjgmyAez0h_I6A5OTUmbFqa5kNvYWA4d-YU,17795 wafer/talks/tests/test_wafer_basic_talks.py,sha256=QcRO6Qbas1muJo8XnU5ueXihafyit8dnUggvWxChzrY,3100 wafer/templates/wafer/base.html,sha256=seuiwWvCPHQK04ZlnuC2557pWRwA6VfcCGdXHY216Ow,1286 wafer/templates/wafer/index.html,sha256=KvrtqeXqmzzeRC3D-Ugo3d7WR0dSI9DoZjOPL5KfNwM,105 wafer/templates/wafer/nav.html,sha256=oTn2TNpyk4u2QswenwpjSuYg12sRaUazlt5EDw3kBCY,2737 wafer/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/tests/test_menu.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/tickets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/tickets/admin.py,sha256=GCak0_lAh995TW-NXWqvabnnu8K9I2nQmYKjtfiS_5I,147 wafer/tickets/forms.py,sha256=Nq2ev_X0CHNe8Cb67qNvvomsgwJK5DiE3PiR5Wg2X1U,1089 wafer/tickets/models.py,sha256=zK5rNBL1bwyHzXAhReOPqXZtmEx3nZsIP9bvOtfQOEw,750 wafer/tickets/urls.py,sha256=r1RHHhLx--7jwqp3kARCwj3xJXjvAyvfgEvixz0L1zU,322 wafer/tickets/views.py,sha256=pfaqV61PgVpX8TvKfDGZYg1OU3weHQzxpgYtnkF0cIQ,4524 wafer/tickets/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/tickets/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/tickets/management/commands/import_quicket_guest_list.py,sha256=gmOoH33NAtOLWMMUk77Bd-qHv3BijkVHHEckq3rXiAw,1196 wafer/tickets/migrations/0001_initial.py,sha256=J2wxgC5OhzJ5LvuGjjtrXi4qErOvBD7LR3gu0tuJJ9E,1512 wafer/tickets/migrations/0002_auto_20150813_1926.py,sha256=MyLVq72PgfHDdGsOqypA0GQkxVj1dXCMcR-xDYgUGXY,394 wafer/tickets/migrations/0003_longer_email_field.py,sha256=rKFqOFTB9lPAAcSU18tJoixwOvJK44PHjJzoJlqyWEI,415 wafer/tickets/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/tickets/templates/wafer.tickets/claim.html,sha256=LYtsnTPzbHrX-bVnBNmLy0hfp1QMxyZC9sUuyUd02NE,753 wafer/tickets/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/tickets/tests/test_views.py,sha256=ntEGJibp5DwVyyXN6ixlZZcBM18Yav1ubHuCBDYJ2q4,5613 wafer/users/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/users/admin.py,sha256=J46q1ubTgDIZ6CpADCltDWBUnS3hyDTmXu-N--hOaSI,591 wafer/users/forms.py,sha256=NayjhBQp7wjKaWiXEPYmjGDMWPBae_gBSShMmRubRL0,3201 wafer/users/models.py,sha256=3PoBHjhPAxIo32X1TCwTX0vx8UAoD2BQIbFtjW1IIAw,2945 wafer/users/renderers.py,sha256=6fWerziAZXPy9m7NbTakdUtQQhwEeAz-un_AZE_4t0M,1324 wafer/users/serializers.py,sha256=ui_pzVB-LJ0GnNrMSzz4qK-1b1HivTNi5GQd2eUUAeU,394 wafer/users/urls.py,sha256=_B_UPwUYes4PtH70qQoGv75B_gWgA1eqItxtFvPMLps,987 wafer/users/views.py,sha256=8qtpJ04eFbgycBU1GtetyyO4lOvCdiNAXfFHH50hrhM,8761 wafer/users/migrations/0001_initial.py,sha256=cg9UiqpLcYsJdiu5S7YGKat96YVfnOxAKNt88iv-d4E,1106 wafer/users/migrations/0002_userprofile_kv.py,sha256=jhQe0aJcOfynYQqiH70mCYL7aS6INOcHdUuIFMcq4dM,426 wafer/users/migrations/0003_auto_20160329_2003.py,sha256=61AosHDpypSdK0uJ2yByYtSi7ZF70akgKe47x1OT8_g,366 wafer/users/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 wafer/users/templates/wafer.users/edit_profile.html,sha256=FtbWirf5g2gMMebOZDx6Sbf75CADeEeldPI3yaM6xck,167 wafer/users/templates/wafer.users/edit_user.html,sha256=aAeo0sziC7GbHbekhKjaR8rFCKnJjnx49abRzv7TYA8,400 wafer/users/templates/wafer.users/profile.html,sha256=s1rG5VTaf7AwN-UYfNi8Y6ABOolX6vX98R-6LKA1FZw,5792 wafer/users/templates/wafer.users/users.html,sha256=QlS403lbHJ0IJF-DCQRfp-IMN1Q-_hnl2tvMs3m_7Q4,1045 wafer/users/templates/wafer.users/registration/confirm_mail.txt,sha256=RKANhD6ZVtmR7WTVqADD_8GwyTO6SUz-9PGWD7TZ790,319 wafer/users/templates/wafer.users/registration/form.html,sha256=eCPaQ_YnyTyPhLRzYoUp1RhoCa7VG4rby0bIQrsCKV4,280 wafer/users/templates/wafer.users/registration/success.html,sha256=VU3qr3CePwIVW6LrQnFko0V-UkRAESd4SHmP_LBHG7Y,1222 wafer/users/tests/test_models.py,sha256=xDoa2-p-z_dgLX-urL0nxtkvqkmPHPqKQRNmHtNHOtM,753 wafer-0.4.0.dist-info/DESCRIPTION.rst,sha256=G4bfOJOQHXU2NAJz_VqmG9BS19PQzFk27DROo-ec14I,3755 wafer-0.4.0.dist-info/METADATA,sha256=plnNiJUR9YVPYFR8Cz3Vo_38rEk5nMydBTg72CqeR3g,5173 wafer-0.4.0.dist-info/RECORD,, wafer-0.4.0.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 wafer-0.4.0.dist-info/metadata.json,sha256=-2EeTC4Um3Y0L5RxzKk2ftbTkkVY53J932NvUhO7nys,1336 wafer-0.4.0.dist-info/top_level.txt,sha256=2MK1IVMWfpLL8BZCQ3E9aG6L6L666gSA_teYlwan4fs,6 PK(HV wafer/urls.pyPK}H Ivxxwafer/context_processors.pyPKOIG},[ wafer/menu.pyPK(HSLwafer/utils.pyPKyI.,]] wafer/__init__.pyPKOIGlB 0!wafer/wsgi.pyPKyIY##&wafer/settings.pyPKv|GKWW,Jwafer/registration/urls.pyPK}H6vOwafer/registration/views.pyPK}HWwafer/registration/forms.pyPKv|GZwafer/registration/apps.pyPKOIG BB[wafer/registration/__init__.pyPK(Hp\wafer/registration/sso.pyPKOIG$ B\twafer/registration/templates/registration/activation_complete.htmlPKOIGҽx}5uwafer/registration/templates/registration/logout.htmlPK}He9@vwafer/registration/templates/registration/registration_form.htmlPKOIGx;\\Dcxwafer/registration/templates/registration/registration_complete.htmlPKOIG@/>!zwafer/registration/templates/registration/activation_email.txtPKv|G'O֦))4}wafer/registration/templates/registration/login.htmlPKOIGY®Fwafer/registration/templates/registration/activation_email_subject.txtPKOIG8ׁ7wafer/registration/templates/registration/activate.htmlPKv|G' @wafer/registration/templates/registration/registration_base.htmlPKOIG+2wafer/registration/templatetags/__init__.pyPKOIGx/{wafer/registration/templatetags/wafer_crispy.pyPKOIG)wafer/registration/migrations/__init__.pyPK}Hwafer/compare/__init__.pyPKyItMMwafer/compare/admin.pyPK}H8,,8wafer/compare/templates/admin/wafer.compare/compare.htmlPK}H{9tt<wafer/compare/templates/admin/wafer.compare/change_form.htmlPK}H,?[p =wafer/compare/templates/admin/wafer.compare/compare_list.htmlPK}H|}}wafer/sponsors/urls.pyPKyI ȷwafer/sponsors/views.pyPKyIܞV Ǽwafer/sponsors/models.pyPKOIG/}  wafer/sponsors/renderers.pyPKOIGLwafer/sponsors/__init__.pyPKKHzUwafer/sponsors/admin.pyPK}Hc11Ywafer/sponsors/serializers.pyPKyIm)||5wafer/sponsors/templates/wafer.sponsors/sponsors.htmlPKyITWW4wafer/sponsors/templates/wafer.sponsors/sponsor.htmlPK}H/Ǐ;=wafer/sponsors/templates/wafer.sponsors/sponsors_block.htmlPKyIjk0[[5;wafer/sponsors/templates/wafer.sponsors/packages.htmlPK}H'wafer/sponsors/templatetags/__init__.pyPK}H{4mm'.wafer/sponsors/templatetags/sponsors.pyPKOIGV%P P )wafer/sponsors/migrations/0001_initial.pyPKKH<N**0wwafer/sponsors/migrations/0002_non-null_files.pyPKOIG%wafer/sponsors/migrations/__init__.pyPKyI42wafer/sponsors/migrations/0004_auto_20160813_1328.pyPK(H5twafer/sponsors/migrations/0003_add_ordering_option.pyPKHqF \wafer/static/js/edit_schedule.jsPKOIGv#wafer/static/js/scheduledatetime.jsPKyI"&±IIYwafer/static/css/wafer.cssPKOIGV(F11)wafer/static/img/glyphicons-halflings.pngPKOIGCI"I"/ Pwafer/static/img/glyphicons-halflings-white.pngPK}Hl rwafer/templates/wafer/nav.htmlPKOIGxBwii }wafer/templates/wafer/index.htmlPK(H"gJ~wafer/templates/wafer/base.htmlPKOIG<wafer/management/static.pyPKOIGrwafer/management/__init__.pyPKcG||2wafer/management/commands/wafer_speaker_tickets.pyPK(H]6O O 7xwafer/management/commands/wafer_registered_attendees.pyPKOIG%wafer/management/commands/__init__.pyPKcG1o53 3 )_wafer/management/commands/wafer_emails.pyPKOIG22(٧wafer/management/commands/wafer_stats.pyPKcG:Qwafer/management/commands/wafer_speaker_contact_details.pyPK}HwgQ,,5wafer/management/commands/wafer_add_default_groups.pyPK}H}Awafer/talks/urls.pyPKyImSлwafer/talks/views.pyPKyIcJ| | wafer/talks/forms.pyPKyI4 wafer/talks/models.pyPKOIGkIIwafer/talks/renderers.pyPKOIGWwafer/talks/__init__.pyPKyIlVVwafer/talks/admin.pyPK}HnUwafer/talks/serializers.pyPKyI(= + wafer/talks/templates/wafer.talks/talk.htmlPKfGc)0"wafer/talks/templates/wafer.talks/talk_form.htmlPKyI_aa/wafer/talks/templates/wafer.talks/speakers.htmlPKHmj}},5wafer/talks/templates/wafer.talks/talks.htmlPKOIGsLnn2$wafer/talks/templates/wafer.talks/talk_delete.htmlPK}H  +'wafer/talks/tests/test_wafer_basic_talks.pyPKOIG4wafer/talks/tests/__init__.pyPKyI@d:#EEZ4wafer/talks/tests/test_views.pyPKyI1zwafer/talks/migrations/0009_auto_20160813_1819.pyPK'HIdm1^}wafer/talks/migrations/0003_talk_private_notes.pyPKOIGi  &wafer/talks/migrations/0001_initial.pyPK}H `<wafer/talks/migrations/0004_edit_private_notes_permission.pyPKH ~-1<wafer/talks/migrations/0008_auto_20160629_1404.pyPKOIG"wafer/talks/migrations/__init__.pyPK(Hml2ڎwafer/talks/migrations/0007_add_ordering_option.pyPK}Hll˹%wafer/talks/migrations/0005_add_kv.pyPK}H;::.wafer/talks/migrations/0006_author_helptext.pyPK}HF1@wafer/talks/migrations/0002_auto_20150813_2327.pyPKOIGwafer/tests/test_menu.pyPKOIGDwafer/tests/__init__.pyPK(H6..ywafer/pages/urls.pyPKyI/.ؚwafer/pages/views.pyPKOIG2R44wafer/pages/forms.pyPK}H͹_lwafer/pages/models.pyPKOIGhMMT``wafer/pages/renderers.pyPKOIGwafer/pages/__init__.pyPK}H rwafer/pages/admin.pyPK}H2 00wafer/pages/serializers.pyPKyIoL+Jwafer/pages/templates/wafer.pages/page.htmlPKyIo3Owafer/pages/templates/wafer.pages/page_compare.htmlPKOIGV0Kwafer/pages/templates/wafer.pages/page_form.htmlPK(HMwafer/pages/tests/test_pages.pyPKKH) UU4wafer/pages/migrations/0003_non-null_people+files.pyPKOIG/yQ~~&swafer/pages/migrations/0001_initial.pyPKOIG"5wafer/pages/migrations/__init__.pyPKOIGq*uwafer/pages/migrations/0002_page_people.pyPK}Hrwafer/users/urls.pyPK(H69"9"~wafer/users/views.pyPK}HGژ wafer/users/forms.pyPK(Hý  wafer/users/models.pyPK(Hx,,P,wafer/users/renderers.pyPKOIG1wafer/users/__init__.pyPKH9XOO1wafer/users/admin.pyPK}H{h4wafer/users/serializers.pyPKyIۙ.*6wafer/users/templates/wafer.users/profile.htmlPK(H1LpG0Mwafer/users/templates/wafer.users/edit_user.htmlPKcGW,Nwafer/users/templates/wafer.users/users.htmlPKOIG3SSwafer/users/templates/wafer.users/edit_profile.htmlPK}H9*???KTwafer/users/templates/wafer.users/registration/confirm_mail.txtPK}H\8Uwafer/users/templates/wafer.users/registration/form.htmlPK(H7';UWwafer/users/templates/wafer.users/registration/success.htmlPK}H[ą t\wafer/users/tests/test_models.pyPKOIG@RR&_wafer/users/migrations/0001_initial.pyPK}HHڪ-9dwafer/users/migrations/0002_userprofile_kv.pyPKOIG".fwafer/users/migrations/__init__.pyPK(H(=D2nn1nfwafer/users/migrations/0003_auto_20160329_2003.pyPKOIG+hwafer/snippets/__init__.pyPK}HM chwafer/snippets/markdown_field.pyPK(HYorwafer/kv/urls.pyPK(H9""swafer/kv/views.pyPK}HVewwafer/kv/utils.pyPK}H}ehhPywafer/kv/models.pyPK}Hzwafer/kv/__init__.pyPK(H|{wafer/kv/permissions.pyPK(H*-?wafer/kv/serializers.pyPK(H%%iwafer/kv/tests/test_kv_api.pyPK(HML%#bwafer/kv/migrations/0001_initial.pyPK(Hwafer/kv/migrations/__init__.pyPKyIMRBBĭwafer/tickets/urls.pyPKyI.0[9wafer/tickets/views.pyPKOIGUlAAwafer/tickets/forms.pyPKOIG{)wafer/tickets/models.pyPKOIGwafer/tickets/__init__.pyPKOIGSwafer/tickets/admin.pyPK}Hyc3a0wafer/tickets/templates/wafer.tickets/claim.htmlPKOIG$wafer/tickets/management/__init__.pyPKOIG?hf>0wafer/tickets/management/commands/import_quicket_guest_list.pyPKOIG-8wafer/tickets/management/commands/__init__.pyPKOIGwafer/tickets/tests/__init__.pyPKyI !wafer/tickets/tests/test_views.pyPK}H 3wafer/tickets/migrations/0003_longer_email_field.pyPKOIGc~(wafer/tickets/migrations/0001_initial.pyPKOIG$ wafer/tickets/migrations/__init__.pyPKOIGp3Lwafer/tickets/migrations/0002_auto_20150813_1926.pyPK|G:'wafer/schedule/urls.pyPKH;))wafer/schedule/views.pyPKH