diff --git a/.gitignore b/.gitignore index 1793954a..b17c3115 100644 --- a/.gitignore +++ b/.gitignore @@ -53,6 +53,7 @@ coverage.xml # Django stuff: *.log +db.sqlite3 # Sphinx documentation docs/_build/ diff --git a/PyRIGS/decorators.py b/PyRIGS/decorators.py index f1023faf..4b897923 100644 --- a/PyRIGS/decorators.py +++ b/PyRIGS/decorators.py @@ -1,5 +1,5 @@ from django.contrib.auth import REDIRECT_FIELD_NAME -from django.shortcuts import render_to_response +from django.shortcuts import render from django.template import RequestContext from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse @@ -26,15 +26,15 @@ def user_passes_test_with_403(test_func, login_url=None, oembed_view=None): return view_func(request, *args, **kwargs) elif not request.user.is_authenticated(): if oembed_view is not None: - extra_context = {} - extra_context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs)) - extra_context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path()) - resp = render_to_response('login_redirect.html', extra_context, context_instance=RequestContext(request)) + context = {} + context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs)) + context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path()) + resp = render(request, 'login_redirect.html', context=context) return resp else: return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path())) else: - resp = render_to_response('403.html', context_instance=RequestContext(request)) + resp = render(request, '403.html') resp.status_code = 403 return resp _checklogin.__doc__ = view_func.__doc__ @@ -62,7 +62,7 @@ def api_key_required(function): userid = kwargs.get('api_pk') key = kwargs.get('api_key') - error_resp = render_to_response('403.html', context_instance=RequestContext(request)) + error_resp = render(request, '403.html') error_resp.status_code = 403 if key is None: diff --git a/PyRIGS/settings.py b/PyRIGS/settings.py index 1cdbcc19..21f36848 100644 --- a/PyRIGS/settings.py +++ b/PyRIGS/settings.py @@ -10,26 +10,31 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os + BASE_DIR = os.path.dirname(os.path.dirname(__file__)) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e' +SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get( + 'SECRET_KEY') else 'gxhy(a#5mhp289_=6xx$7jh=eh$ymxg^ymc+di*0c*geiu3p_e' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True -STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False -TEMPLATE_DEBUG = True +STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] if STAGING: ALLOWED_HOSTS.append('.herokuapp.com') +if DEBUG: + ALLOWED_HOSTS.append('localhost') + ALLOWED_HOSTS.append('example.com') + SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') if not DEBUG: SECURE_SSL_REDIRECT = True # Redirect all http requests to https @@ -40,7 +45,6 @@ ADMINS = ( ('Tom Price', 'tomtom5152@gmail.com') ) - # Application definition INSTALLED_APPS = ( @@ -63,6 +67,7 @@ INSTALLED_APPS = ( MIDDLEWARE_CLASSES = ( 'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware', 'django.middleware.security.SecurityMiddleware', + 'debug_toolbar.middleware.DebugToolbarMiddleware', 'reversion.middleware.RevisionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -77,7 +82,6 @@ ROOT_URLCONF = 'PyRIGS.urls' WSGI_APPLICATION = 'PyRIGS.wsgi.application' - # Database # https://docs.djangoproject.com/en/1.7/ref/settings/#databases DATABASES = { @@ -89,6 +93,7 @@ DATABASES = { if not DEBUG: import dj_database_url + DATABASES['default'] = dj_database_url.config() # Logging @@ -119,12 +124,12 @@ LOGGING = { 'mail_admins': { 'class': 'django.utils.log.AdminEmailHandler', 'level': 'ERROR', - # But the emails are plain text by default - HTML is nicer + # But the emails are plain text by default - HTML is nicer 'include_html': True, }, }, 'loggers': { - # Again, default Django configuration to email unhandled exceptions + # Again, default Django configuration to email unhandled exceptions 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', @@ -152,8 +157,8 @@ RAVEN_CONFIG = { AUTH_USER_MODEL = 'RIGS.Profile' LOGIN_REDIRECT_URL = '/' -LOGIN_URL = '/user/login' -LOGOUT_URL = '/user/logout' +LOGIN_URL = '/user/login/' +LOGOUT_URL = '/user/logout/' ACCOUNT_ACTIVATION_DAYS = 7 @@ -167,7 +172,7 @@ EMAILER_TEST = False if not DEBUG or EMAILER_TEST: EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' EMAIL_HOST = os.environ.get('EMAIL_HOST') - EMAIL_PORT = int(os.environ.get('EMAIL_PORT')) + EMAIL_PORT = int(os.environ.get('EMAIL_PORT', 25)) EMAIL_HOST_USER = os.environ.get('EMAIL_HOST_USER') EMAIL_HOST_PASSWORD = os.environ.get('EMAIL_HOST_PASSWORD') EMAIL_USE_TLS = bool(int(os.environ.get('EMAIL_USE_TLS', 0))) @@ -191,19 +196,7 @@ USE_L10N = True USE_TZ = True -DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M','%Y-%m-%dT%H:%M:%S') - -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.core.context_processors.request", - "django.contrib.messages.context_processors.messages", -) - +DATETIME_INPUT_FORMATS = ('%Y-%m-%dT%H:%M', '%Y-%m-%dT%H:%M:%S') # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.7/howto/static-files/ @@ -214,11 +207,30 @@ STATIC_DIRS = ( os.path.join(BASE_DIR, 'static/') ) -TEMPLATE_DIRS = ( - os.path.join(BASE_DIR, 'templates'), -) +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [ + os.path.join(BASE_DIR, 'templates'), + ], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.template.context_processors.request", + "django.contrib.messages.context_processors.messages", + ], + 'debug': DEBUG + }, + }, +] -USE_GRAVATAR=True +USE_GRAVATAR = True TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf" AUTHORISATION_NOTIFICATION_ADDRESS = 'productions@nottinghamtec.co.uk' diff --git a/PyRIGS/urls.py b/PyRIGS/urls.py index 9821ae20..65bf2e63 100644 --- a/PyRIGS/urls.py +++ b/PyRIGS/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import patterns, include, url +from django.conf.urls import include, url from django.contrib import admin from django.contrib.staticfiles.urls import staticfiles_urlpatterns from django.conf import settings @@ -6,19 +6,24 @@ from registration.backends.default.views import RegistrationView import RIGS from RIGS import regbackend -urlpatterns = patterns('', +urlpatterns = [ # Examples: # url(r'^$', 'PyRIGS.views.home', name='home'), # url(r'^blog/', include('blog.urls')), url(r'^', include('RIGS.urls')), - url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail), + url('^user/register/$', RegistrationView.as_view(form_class=RIGS.forms.ProfileRegistrationFormUniqueEmail), name="registration_register"), url('^user/', include('django.contrib.auth.urls')), url('^user/', include('registration.backends.default.urls')), url(r'^admin/', include(admin.site.urls)), -) +] if settings.DEBUG: - urlpatterns += staticfiles_urlpatterns() \ No newline at end of file + urlpatterns += staticfiles_urlpatterns() + + import debug_toolbar + urlpatterns = [ + url(r'^__debug__/', include(debug_toolbar.urls)), + ] + urlpatterns diff --git a/README.md b/README.md index 787502ef..625bac90 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # TEC PA & Lighting - PyRIGS # [![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg?branch=develop)](https://travis-ci.org/nottinghamtec/PyRIGS) [![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop) +[![Dependency Status](https://gemnasium.com/badges/github.com/nottinghamtec/PyRIGS.svg)](https://gemnasium.com/github.com/nottinghamtec/PyRIGS) + Welcome to TEC PA & Lightings PyRIGS program. This is a reimplementation of the existing Rig Information Gathering System (RIGS) that was developed using Ruby on Rails. diff --git a/RIGS/admin.py b/RIGS/admin.py index a351aed0..49b8aa1e 100644 --- a/RIGS/admin.py +++ b/RIGS/admin.py @@ -2,7 +2,7 @@ from django.contrib import admin from RIGS import models, forms from django.contrib.auth.admin import UserAdmin from django.utils.translation import ugettext_lazy as _ -import reversion +from reversion.admin import VersionAdmin from django.contrib.admin import helpers from django.template.response import TemplateResponse @@ -12,10 +12,12 @@ from django.core.exceptions import ObjectDoesNotExist from django.db.models import Count from django.forms import ModelForm +from reversion import revisions as reversion + # Register your models here. -admin.site.register(models.VatRate, reversion.VersionAdmin) -admin.site.register(models.Event, reversion.VersionAdmin) -admin.site.register(models.EventItem, reversion.VersionAdmin) +admin.site.register(models.VatRate, VersionAdmin) +admin.site.register(models.Event, VersionAdmin) +admin.site.register(models.EventItem, VersionAdmin) admin.site.register(models.Invoice) admin.site.register(models.Payment) @@ -41,7 +43,7 @@ class ProfileAdmin(UserAdmin): add_form = forms.ProfileCreationForm -class AssociateAdmin(reversion.VersionAdmin): +class AssociateAdmin(VersionAdmin): list_display = ('id', 'name', 'number_of_events') search_fields = ['id', 'name'] list_display_links = ['id', 'name'] @@ -93,8 +95,7 @@ class AssociateAdmin(reversion.VersionAdmin): 'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME, 'forms': forms } - return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context, - current_app=self.admin_site.name) + return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context) @admin.register(models.Person) diff --git a/RIGS/finance.py b/RIGS/finance.py index 1e0c91a7..2c9f21c0 100644 --- a/RIGS/finance.py +++ b/RIGS/finance.py @@ -15,6 +15,9 @@ from z3c.rml import rml2pdf from RIGS import models +from django import forms +forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) + class InvoiceIndex(generic.ListView): model = models.Invoice @@ -55,8 +58,8 @@ class InvoicePrint(generic.View): invoice = get_object_or_404(models.Invoice, pk=pk) object = invoice.event template = get_template('RIGS/event_print.xml') - copies = ('TEC', 'Client') - context = RequestContext(request, { + + context = { 'object': object, 'fonts': { 'opensans': { @@ -66,7 +69,7 @@ class InvoicePrint(generic.View): }, 'invoice': invoice, 'current_user': request.user, - }) + } rml = template.render(context) buffer = StringIO.StringIO() @@ -78,7 +81,7 @@ class InvoicePrint(generic.View): escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name) response = HttpResponse(content_type='application/pdf') - response['Content-Disposition'] = "filename=Invoice %05d | %s.pdf" % (invoice.pk, escapedEventName) + response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName) response.write(pdfData) return response diff --git a/RIGS/forms.py b/RIGS/forms.py index 752b7fb8..5d5fb0e6 100644 --- a/RIGS/forms.py +++ b/RIGS/forms.py @@ -10,6 +10,10 @@ import simplejson from RIGS import models +# Override the django form defaults to use the HTML date/time/datetime UI elements +forms.DateField.widget = forms.DateInput(attrs={'type': 'date'}) +forms.TimeField.widget = forms.DateInput(attrs={'type': 'time'}) +forms.DateTimeField.widget = forms.DateInput(attrs={'type': 'datetime-local'}) # Registration class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail): @@ -45,7 +49,7 @@ class ProfileChangeForm(UserChangeForm): # Events Shit class EventForm(forms.ModelForm): - datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + settings.DATETIME_INPUT_FORMATS + datetime_input_formats = formats.get_format_lazy("DATETIME_INPUT_FORMATS") + list(settings.DATETIME_INPUT_FORMATS) meet_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False) access_at = forms.DateTimeField(input_formats=datetime_input_formats, required=False) diff --git a/RIGS/management/commands/generateSampleData.py b/RIGS/management/commands/generateSampleData.py index bf1ce7d2..59c39c97 100644 --- a/RIGS/management/commands/generateSampleData.py +++ b/RIGS/management/commands/generateSampleData.py @@ -1,7 +1,7 @@ from django.core.management.base import BaseCommand, CommandError from django.contrib.auth.models import Group, Permission from django.db import transaction -import reversion +from reversion import revisions as reversion import datetime import random diff --git a/RIGS/migrations/0025_auto_20160331_1302.py b/RIGS/migrations/0025_auto_20160331_1302.py new file mode 100644 index 00000000..eacc7bfd --- /dev/null +++ b/RIGS/migrations/0025_auto_20160331_1302.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.4 on 2016-03-31 12:02 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0024_auto_20160229_2042'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, unique=True, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')], verbose_name='username'), + ), + migrations.AlterField( + model_name='vatrate', + name='start_at', + field=models.DateField(), + ), + ] diff --git a/RIGS/migrations/0026_auto_20170510_1846.py b/RIGS/migrations/0026_auto_20170510_1846.py new file mode 100644 index 00000000..0a350f10 --- /dev/null +++ b/RIGS/migrations/0026_auto_20170510_1846.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.1 on 2017-05-10 17:46 +from __future__ import unicode_literals + +import django.contrib.auth.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('RIGS', '0025_auto_20160331_1302'), + ] + + operations = [ + migrations.AlterField( + model_name='profile', + name='username', + field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'), + ), + ] diff --git a/RIGS/models.py b/RIGS/models.py index 89ec852b..ebb31ca6 100644 --- a/RIGS/models.py +++ b/RIGS/models.py @@ -1,19 +1,22 @@ import datetime import hashlib -import pytz -import random +import datetime, pytz + +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.conf import settings +from django.utils import timezone +from django.utils.functional import cached_property +from django.utils.encoding import python_2_unicode_compatible +from reversion import revisions as reversion import string + +import random from collections import Counter from decimal import Decimal -import reversion -from django.conf import settings -from django.contrib.auth.models import AbstractUser from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse_lazy -from django.db import models -from django.utils.encoding import python_2_unicode_compatible -from django.utils.functional import cached_property # Create your models here. @@ -175,7 +178,7 @@ class Organisation(models.Model, RevisionMixin): class VatManager(models.Manager): def current_rate(self): - return self.find_rate(datetime.datetime.now()) + return self.find_rate(timezone.now()) def find_rate(self, date): # return self.filter(startAt__lte=date).latest() @@ -190,7 +193,7 @@ class VatManager(models.Manager): @reversion.register @python_2_unicode_compatible class VatRate(models.Model, RevisionMixin): - start_at = models.DateTimeField() + start_at = models.DateField() rate = models.DecimalField(max_digits=6, decimal_places=6) comment = models.CharField(max_length=255) @@ -241,18 +244,12 @@ class Venue(models.Model, RevisionMixin): class EventManager(models.Manager): def current_events(self): events = self.filter( - (models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q( - status=Event.CANCELLED)) | # Starts after with no end - (models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q( - status=Event.CANCELLED)) | # Ends after - (models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q( - status=Event.CANCELLED)) | # Active dry hire - (models.Q(dry_hire=True, checked_in_by__isnull=True) & ( - models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT - models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started - ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', - 'organisation', - 'venue', 'mic') + (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end + (models.Q(end_date__gte=timezone.now().date(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after + (models.Q(dry_hire=True, start_date__gte=timezone.now().date()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire + (models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT + models.Q(status=Event.CANCELLED, start_date__gte=timezone.now().date()) # Canceled but not started + ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') return events def events_in_bounds(self, start, end): @@ -275,12 +272,12 @@ class EventManager(models.Manager): def rig_count(self): event_count = self.filter( - (models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False, + (models.Q(start_date__gte=timezone.now().date(), end_date__isnull=True, dry_hire=False, is_rig=True) & ~models.Q( status=Event.CANCELLED)) | # Starts after with no end - (models.Q(end_date__gte=datetime.date.today(), dry_hire=False, is_rig=True) & ~models.Q( + (models.Q(end_date__gte=timezone.now().date(), dry_hire=False, is_rig=True) & ~models.Q( status=Event.CANCELLED)) | # Ends after - (models.Q(dry_hire=True, start_date__gte=datetime.date.today(), is_rig=True) & ~models.Q( + (models.Q(dry_hire=True, start_date__gte=timezone.now().date(), is_rig=True) & ~models.Q( status=Event.CANCELLED)) | # Active dry hire (models.Q(dry_hire=True, checked_in_by__isnull=True, is_rig=True) & ( models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) # Active dry hire GT diff --git a/RIGS/rigboard.py b/RIGS/rigboard.py index 80c3c4b8..dbe7e6c7 100644 --- a/RIGS/rigboard.py +++ b/RIGS/rigboard.py @@ -151,7 +151,7 @@ class EventPrint(generic.View): merger = PdfFileMerger() - context = RequestContext(request, { + context = { 'object': object, 'fonts': { 'opensans': { @@ -161,7 +161,7 @@ class EventPrint(generic.View): }, 'quote': True, 'current_user': request.user, - }) + } rml = template.render(context) diff --git a/RIGS/templates/RIGS/event_form.html b/RIGS/templates/RIGS/event_form.html index 71e91fe6..24f6aa78 100644 --- a/RIGS/templates/RIGS/event_form.html +++ b/RIGS/templates/RIGS/event_form.html @@ -289,10 +289,10 @@
- {% render_field form.start_date type="date" class+="form-control" %} + {% render_field form.start_date class+="form-control" %}
- {% render_field form.start_time type="time" class+="form-control" %} + {% render_field form.start_time class+="form-control" %}
@@ -304,10 +304,10 @@
- {% render_field form.end_date type="date" class+="form-control" %} + {% render_field form.end_date class+="form-control" %}
- {% render_field form.end_time type="time" class+="form-control" %} + {% render_field form.end_time class+="form-control" %}
@@ -329,7 +329,7 @@ class="col-sm-4 control-label">{{ form.access_at.label }}
- {% render_field form.access_at type="datetime-local" class+="form-control" %} + {% render_field form.access_at class+="form-control" %}
@@ -337,7 +337,7 @@ class="col-sm-4 control-label">{{ form.meet_at.label }}
- {% render_field form.meet_at type="datetime-local" class+="form-control" %} + {% render_field form.meet_at class+="form-control" %}
diff --git a/RIGS/templates/RIGS/payment_form.html b/RIGS/templates/RIGS/payment_form.html index 41bbf15b..13f4a694 100644 --- a/RIGS/templates/RIGS/payment_form.html +++ b/RIGS/templates/RIGS/payment_form.html @@ -16,7 +16,7 @@ for="{{ form.date.id_for_label }}">{{ form.date.label }}
- {% render_field form.date type="date" class+="form-control" %} + {% render_field form.date class+="form-control" %}
diff --git a/RIGS/test_functional.py b/RIGS/test_functional.py index 96d074ca..231ade20 100644 --- a/RIGS/test_functional.py +++ b/RIGS/test_functional.py @@ -18,6 +18,14 @@ from selenium.webdriver.support.ui import WebDriverWait from RIGS import models +import re +import os +from datetime import date, timedelta +from django.db import transaction +from reversion import revisions as reversion +import json + + class UserRegistrationTest(LiveServerTestCase): def setUp(self): @@ -434,7 +442,8 @@ class EventTest(LiveServerTestCase): # See redirected to success page successTitle = self.browser.find_element_by_xpath('//h1').text event = models.Event.objects.get(name='Test Event Name') - self.assertIn("N0000%d | Test Event Name" % event.pk, successTitle) + + self.assertIn("N%05d | Test Event Name"%event.pk, successTitle) except WebDriverException: # This is a dirty workaround for wercker being a bit funny and not running it correctly. # Waiting for wercker to get back to me about this @@ -496,9 +505,9 @@ class EventTest(LiveServerTestCase): # Attempt to save save.click() - self.assertNotIn("N0000%d" % testEvent.pk, self.browser.find_element_by_xpath('//h1').text) - self.assertNotIn("Event data duplicated but not yet saved", - self.browser.find_element_by_id('content').text) # Check info message not visible + + self.assertNotIn("N%05d"%testEvent.pk, self.browser.find_element_by_xpath('//h1').text) + self.assertNotIn("Event data duplicated but not yet saved", self.browser.find_element_by_id('content').text) # Check info message not visible # Check the new items are visible table = self.browser.find_element_by_id('item-table') # ID number is known, see above @@ -507,15 +516,22 @@ class EventTest(LiveServerTestCase): self.assertIn("Test Item 3", table.text) infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..') - self.assertIn("N0000%d" % testEvent.pk, - infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text) - self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk)) # Go back to the old event - # Check that based-on hasn't crept into the old event + self.assertIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text) + + # Check the PO hasn't carried through + self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text) + + self.browser.get(self.live_server_url + '/event/' + str(testEvent.pk)) #Go back to the old event + + #Check that based-on hasn't crept into the old event infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..') - self.assertNotIn("N0000%d" % testEvent.pk, - infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text) + + self.assertNotIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text) + + # Check the PO remains on the old event + self.assertIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text) # Check the items are as they were table = self.browser.find_element_by_id('item-table') # ID number is known, see above @@ -630,8 +646,9 @@ class EventTest(LiveServerTestCase): # See redirected to success page successTitle = self.browser.find_element_by_xpath('//h1').text event = models.Event.objects.get(name='Test Event Name') - self.assertIn("N0000%d | Test Event Name" % event.pk, successTitle) + self.assertIn("N%05d | Test Event Name"%event.pk, successTitle) + def testRigNonRig(self): self.browser.get(self.live_server_url + '/event/create/') # Gets redirected to login and back diff --git a/RIGS/test_models.py b/RIGS/test_models.py index 1f36be43..1fd8b4a0 100644 --- a/RIGS/test_models.py +++ b/RIGS/test_models.py @@ -17,71 +17,82 @@ class ProfileTestCase(TestCase): class VatRateTestCase(TestCase): - def setUp(self): - models.VatRate.objects.create(start_at='2014-03-01', rate=0.20, comment='test1') - models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2') + @classmethod + def setUpTestData(cls): + cls.rates = { + 0: models.VatRate.objects.create(start_at='2014-03-01', rate=0.20, comment='test1'), + 1: models.VatRate.objects.create(start_at='2016-03-01', rate=0.15, comment='test2'), + } def test_find_correct(self): r = models.VatRate.objects.find_rate('2015-03-01') - self.assertEqual(r.comment, 'test1') + self.assertEqual(r, self.rates[0]) r = models.VatRate.objects.find_rate('2016-03-01') - self.assertEqual(r.comment, 'test2') + self.assertEqual(r, self.rates[1]) def test_percent_correct(self): - r = models.VatRate.objects.get(rate=0.20) - self.assertEqual(r.as_percent, 20) + self.assertEqual(self.rates[0].as_percent, 20) class EventTestCase(TestCase): - def setUp(self): - self.all_events = set(range(1, 18)) - self.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18) - self.not_current_events = set(self.all_events) - set(self.current_events) + @classmethod + def setUpTestData(cls): + cls.all_events = set(range(1, 18)) + cls.current_events = (1, 2, 3, 6, 7, 8, 10, 11, 12, 14, 15, 16, 18) + cls.not_current_events = set(cls.all_events) - set(cls.current_events) - self.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') - self.profile = models.Profile.objects.create(username="testuser1", email="1@test.com") + cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com") - # produce 7 normal events - 5 current - models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6), - description="start future no end") - models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end") - models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(), - description="start today with end today") - models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end") - models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21', - description="start past with end past") - models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2), - end_date=date.today() + timedelta(days=2), description="start past, end future") - models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2), - end_date=date.today() + timedelta(days=2), description="start + end in future") + cls.events = { + # produce 7 normal events - 5 current + 1: models.Event.objects.create(name="TE E1", start_date=date.today() + timedelta(days=6), + description="start future no end"), + 2: models.Event.objects.create(name="TE E2", start_date=date.today(), description="start today no end"), + 3: models.Event.objects.create(name="TE E3", start_date=date.today(), end_date=date.today(), + description="start today with end today"), + 4: models.Event.objects.create(name="TE E4", start_date='2014-03-20', description="start past no end"), + 5: models.Event.objects.create(name="TE E5", start_date='2014-03-20', end_date='2014-03-21', + description="start past with end past"), + 6: models.Event.objects.create(name="TE E6", start_date=date.today() - timedelta(days=2), + end_date=date.today() + timedelta(days=2), + description="start past, end future"), + 7: models.Event.objects.create(name="TE E7", start_date=date.today() + timedelta(days=2), + end_date=date.today() + timedelta(days=2), + description="start + end in future"), - # 2 cancelled - 1 current - models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2), - end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, - description="cancelled in future") - models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1), - end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, - description="cancelled and started") + # 2 cancelled - 1 current + 8: models.Event.objects.create(name="TE E8", start_date=date.today() + timedelta(days=2), + end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, + description="cancelled in future"), + 9: models.Event.objects.create(name="TE E9", start_date=date.today() - timedelta(days=1), + end_date=date.today() + timedelta(days=2), status=models.Event.CANCELLED, + description="cancelled and started"), - # 5 dry hire - 3 current - models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, description="dryhire today") - models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, checked_in_by=self.profile, - description="dryhire today, checked in") - models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True, - status=models.Event.BOOKED, description="dryhire past") - models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True, - checked_in_by=self.profile, description="dryhire past checked in") - models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True, - status=models.Event.CANCELLED, description="dryhire today cancelled") + # 5 dry hire - 3 current + 10: models.Event.objects.create(name="TE E10", start_date=date.today(), dry_hire=True, + description="dryhire today"), + 11: models.Event.objects.create(name="TE E11", start_date=date.today(), dry_hire=True, + checked_in_by=cls.profile, + description="dryhire today, checked in"), + 12: models.Event.objects.create(name="TE E12", start_date=date.today() - timedelta(days=1), dry_hire=True, + status=models.Event.BOOKED, description="dryhire past"), + 13: models.Event.objects.create(name="TE E13", start_date=date.today() - timedelta(days=2), dry_hire=True, + checked_in_by=cls.profile, description="dryhire past checked in"), + 14: models.Event.objects.create(name="TE E14", start_date=date.today(), dry_hire=True, + status=models.Event.CANCELLED, description="dryhire today cancelled"), - # 4 non rig - 3 current - models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, description="non rig today") - models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False, - description="non rig tomorrow") - models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False, - description="non rig yesterday") - models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, status=models.Event.CANCELLED, - description="non rig today cancelled") + # 4 non rig - 3 current + 15: models.Event.objects.create(name="TE E15", start_date=date.today(), is_rig=False, + description="non rig today"), + 16: models.Event.objects.create(name="TE E16", start_date=date.today() + timedelta(days=1), is_rig=False, + description="non rig tomorrow"), + 17: models.Event.objects.create(name="TE E17", start_date=date.today() - timedelta(days=1), is_rig=False, + description="non rig yesterday"), + 18: models.Event.objects.create(name="TE E18", start_date=date.today(), is_rig=False, + status=models.Event.CANCELLED, + description="non rig today cancelled"), + } def test_count(self): # Santiy check we have the expected events created @@ -103,17 +114,23 @@ class EventTestCase(TestCase): def test_related_venue(self): v1 = models.Venue.objects.create(name="TE V1") v2 = models.Venue.objects.create(name="TE V2") - events = models.Event.objects.all() - for event in events[:2]: - event.venue = v1 - event.save() - for event in events[3:4]: - event.venue = v2 + + e1 = [] + e2 = [] + for (key, event) in self.events.iteritems(): + if event.pk % 2: + event.venue = v1 + e1.append(event) + else: + event.venue = v2 + e2.append(event) event.save() - events = models.Event.objects.all() - self.assertItemsEqual(events[:2], v1.latest_events) - self.assertItemsEqual(events[3:4], v2.latest_events) + self.assertItemsEqual(e1, v1.latest_events) + self.assertItemsEqual(e2, v2.latest_events) + + for (key, event) in self.events.iteritems(): + event.venue = None def test_related_vatrate(self): self.assertEqual(self.vatrate, models.Event.objects.all()[0].vat_rate) @@ -122,33 +139,43 @@ class EventTestCase(TestCase): p1 = models.Person.objects.create(name="TE P1") p2 = models.Person.objects.create(name="TE P2") - events = models.Event.objects.all() - for event in events[:2]: - event.person = p1 - event.save() - for event in events[3:4]: - event.person = p2 + e1 = [] + e2 = [] + for (key, event) in self.events.iteritems(): + if event.pk % 2: + event.person = p1 + e1.append(event) + else: + event.person = p2 + e2.append(event) event.save() - events = models.Event.objects.all() - self.assertItemsEqual(events[:2], p1.latest_events) - self.assertItemsEqual(events[3:4], p2.latest_events) + self.assertItemsEqual(e1, p1.latest_events) + self.assertItemsEqual(e2, p2.latest_events) + + for (key, event) in self.events.iteritems(): + event.person = None def test_related_organisation(self): o1 = models.Organisation.objects.create(name="TE O1") o2 = models.Organisation.objects.create(name="TE O2") - events = models.Event.objects.all() - for event in events[:2]: - event.organisation = o1 - event.save() - for event in events[3:4]: - event.organisation = o2 + e1 = [] + e2 = [] + for (key, event) in self.events.iteritems(): + if event.pk % 2: + event.organisation = o1 + e1.append(event) + else: + event.organisation = o2 + e2.append(event) event.save() - events = models.Event.objects.all() - self.assertItemsEqual(events[:2], o1.latest_events) - self.assertItemsEqual(events[3:4], o2.latest_events) + self.assertItemsEqual(e1, o1.latest_events) + self.assertItemsEqual(e2, o2.latest_events) + + for (key, event) in self.events.iteritems(): + event.organisation = None def test_organisation_person_join(self): p1 = models.Person.objects.create(name="TE P1") @@ -186,20 +213,20 @@ class EventTestCase(TestCase): self.assertEqual(len(o2.persons), 1) def test_cancelled_property(self): - event = models.Event.objects.all()[0] - event.status = models.Event.CANCELLED - event.save() - event = models.Event.objects.all()[0] + edit = self.events[1] + edit.status = models.Event.CANCELLED + edit.save() + event = models.Event.objects.get(pk=edit.pk) self.assertEqual(event.status, models.Event.CANCELLED) self.assertTrue(event.cancelled) event.status = models.Event.PROVISIONAL event.save() def test_confirmed_property(self): - event = models.Event.objects.all()[0] - event.status = models.Event.CONFIRMED - event.save() - event = models.Event.objects.all()[0] + edit = self.events[1] + edit.status = models.Event.CONFIRMED + edit.save() + event = models.Event.objects.get(pk=edit.pk) self.assertEqual(event.status, models.Event.CONFIRMED) self.assertTrue(event.confirmed) event.status = models.Event.PROVISIONAL @@ -250,14 +277,14 @@ class EventTestCase(TestCase): # basic checks manager.create(name='TE IB2', start_date='2016-01-02', end_date='2016-01-04'), manager.create(name='TE IB3', start_date='2015-12-31', end_date='2016-01-03'), - manager.create(name='TE IB4', start_date='2016-01-04', access_at='2016-01-03'), - manager.create(name='TE IB5', start_date='2016-01-04', meet_at='2016-01-02'), + manager.create(name='TE IB4', start_date='2016-01-04', access_at=self.create_datetime(2016, 01, 03, 00, 00)), + manager.create(name='TE IB5', start_date='2016-01-04', meet_at=self.create_datetime(2016, 01, 02, 00, 00)), # negative check manager.create(name='TE IB6', start_date='2015-12-31', end_date='2016-01-01'), ] - in_bounds = manager.events_in_bounds(datetime(2016, 1, 2), datetime(2016, 1, 3)) + in_bounds = manager.events_in_bounds(self.create_datetime(2016, 1, 2, 0, 0), self.create_datetime(2016, 1, 3, 0, 0)) self.assertIn(events[0], in_bounds) self.assertIn(events[1], in_bounds) self.assertIn(events[2], in_bounds) diff --git a/RIGS/test_unit.py b/RIGS/test_unit.py index 82a7acca..5b531636 100644 --- a/RIGS/test_unit.py +++ b/RIGS/test_unit.py @@ -164,6 +164,8 @@ class TestInvoiceDelete(TestCase): def setUpTestData(cls): cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True) + cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + cls.events = { 1: models.Event.objects.create(name="TE E1", start_date=date.today()), 2: models.Event.objects.create(name="TE E2", start_date=date.today()) @@ -214,6 +216,39 @@ class TestInvoiceDelete(TestCase): self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk)) +class TestPrintPaperwork(TestCase): + @classmethod + def setUpTestData(cls): + cls.profile = models.Profile.objects.create(username="testuser1", email="1@test.com", is_superuser=True, is_active=True, is_staff=True) + + cls.vatrate = models.VatRate.objects.create(start_at='2014-03-05', rate=0.20, comment='test1') + + cls.events = { + 1: models.Event.objects.create(name="TE E1", start_date=date.today()), + } + + cls.invoices = { + 1: models.Invoice.objects.create(event=cls.events[1]), + } + + def setUp(self): + self.profile.set_password('testuser') + self.profile.save() + self.assertTrue(self.client.login(username=self.profile.username, password='testuser')) + + def test_print_paperwork_success(self): + request_url = reverse('event_print', kwargs={'pk': self.events[1].pk}) + + response = self.client.get(request_url, follow=True) + self.assertEqual(response.status_code, 200) + + def test_print_invoice_success(self): + request_url = reverse('invoice_print', kwargs={'pk': self.invoices[1].pk}) + + response = self.client.get(request_url, follow=True) + self.assertEqual(response.status_code, 200) + + class TestEmbeddedViews(TestCase): @classmethod def setUpTestData(cls): diff --git a/RIGS/urls.py b/RIGS/urls.py index 9c9f1628..4d1c67ec 100644 --- a/RIGS/urls.py +++ b/RIGS/urls.py @@ -1,4 +1,6 @@ -from django.conf.urls import patterns, url +from django.conf.urls import url +from django.contrib.auth.views import password_reset + from django.contrib.auth.decorators import login_required from RIGS import models, views, rigboard, finance, ical, versioning, forms from django.views.generic import RedirectView @@ -7,17 +9,17 @@ from django.views.decorators.clickjacking import xframe_options_exempt from PyRIGS.decorators import permission_required_with_403 from PyRIGS.decorators import api_key_required -urlpatterns = patterns('', +urlpatterns = [ # Examples: # url(r'^$', 'PyRIGS.views.home', name='home'), # url(r'^blog/', include('blog.urls')), url('^$', login_required(views.Index.as_view()), name='index'), url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'), - url('^user/login/$', 'RIGS.views.login', name='login'), + url('^user/login/$', views.login, name='login'), url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'), - url(r'^user/password_reset/$', 'django.contrib.auth.views.password_reset', - {'password_reset_form': forms.PasswordReset}), + + url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}), # People url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()), @@ -188,4 +190,4 @@ urlpatterns = patterns('', RedirectView.as_view(permanent=True, pattern_name='event_detail')), url(r'^bookings/$', RedirectView.as_view(permanent=True, pattern_name='rigboard')), url(r'^bookings/past/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')), - ) + ] diff --git a/RIGS/versioning.py b/RIGS/versioning.py index 6d524fc0..65d4328a 100644 --- a/RIGS/versioning.py +++ b/RIGS/versioning.py @@ -158,7 +158,7 @@ def get_previous_version(version): thisId = version.object_id thisVersionId = version.pk - versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId) + versions = reversion.revisions.get_for_object_reference(version.content_type.model_class(), thisId) try: previousVersions = versions.filter(revision_id__lt=version.revision_id).latest( @@ -199,7 +199,7 @@ def get_changes_for_version(newVersion, oldVersion=None): class VersionHistory(generic.ListView): - model = reversion.revisions.Version + model = Version template_name = "RIGS/version_history.html" paginate_by = 25 @@ -207,7 +207,7 @@ class VersionHistory(generic.ListView): thisModel = self.kwargs['model'] # thisObject = get_object_or_404(thisModel, pk=self.kwargs['pk']) - versions = reversion.get_for_object_reference(thisModel, self.kwargs['pk']) + versions = reversion.revisions.get_for_object_reference(thisModel, self.kwargs['pk']) return versions @@ -236,7 +236,7 @@ class VersionHistory(generic.ListView): class ActivityTable(generic.ListView): - model = reversion.revisions.Version + model = Version template_name = "RIGS/activity_table.html" paginate_by = 25 @@ -260,7 +260,7 @@ class ActivityTable(generic.ListView): class ActivityFeed(generic.ListView): - model = reversion.revisions.Version + model = Version template_name = "RIGS/activity_feed_data.html" paginate_by = 25 diff --git a/RIGS/views.py b/RIGS/views.py index c0186bed..b681c1bb 100644 --- a/RIGS/views.py +++ b/RIGS/views.py @@ -30,7 +30,7 @@ class Index(generic.TemplateView): def login(request, **kwargs): if request.user.is_authenticated(): - next = request.REQUEST.get('next', '/') + next = request.GET.get('next', '/') return HttpResponseRedirect(next) else: from django.contrib.auth.views import login @@ -44,9 +44,8 @@ def login(request, **kwargs): # check for it before logging the user in @csrf_exempt def login_embed(request, **kwargs): - print("Running LOGIN") if request.user.is_authenticated(): - next = request.REQUEST.get('next', '/') + next = request.GET.get('next', '/') return HttpResponseRedirect(next) else: from django.contrib.auth.views import login diff --git a/db.sqlite3 b/db.sqlite3 deleted file mode 100644 index 07462b3f..00000000 Binary files a/db.sqlite3 and /dev/null differ diff --git a/requirements.txt b/requirements.txt index 4bfce216..a285e33e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,34 +1,37 @@ +beautifulsoup4==4.6.0 +contextlib2==0.5.5 diff-match-patch==20121119 -dj-database-url==0.3.0 +dj-database-url==0.4.2 dj-static==0.0.6 -Django==1.8.2 -django-debug-toolbar==1.3.0 -django-ical==1.3 -django-recaptcha==1.0.4 -django-registration-redux==1.2 -django-reversion==1.8.7 +Django==1.11.1 +django-debug-toolbar==1.8 +django-ical==1.4 +django-recaptcha==1.3.0 +django-registration-redux==1.6 +django-reversion==1.10.2 django-toolbelt==0.0.1 -django-widget-tweaks==1.3 -gunicorn==19.3.0 -icalendar==3.9.0 -lxml==3.4.4 -Pillow==2.8.1 premailer==3.0.1 -psycopg2==2.6 -Pygments==2.0.2 -PyPDF2==1.24 -python-dateutil==2.4.2 -pytz==2015.4 -raven==5.8.1 -reportlab==3.1.44 -selenium==2.53.6 -simplejson==3.7.2 -six==1.9.0 -sqlparse==0.1.15 -static3==0.6.1 +django-widget-tweaks==1.4.1 +gunicorn==19.7.1 +icalendar==3.11.4 +lxml==3.7.3 +Markdown==2.6.8 +Pillow==4.1.1 +psycopg2==2.7.1 +Pygments==2.2.0 +PyPDF2==1.26.0 +python-dateutil==2.6.0 +pytz==2017.2 +raven==6.0.0 +reportlab==3.4.0 +selenium==2.53.1 +simplejson==3.10.0 +six==1.10.0 +sqlparse==0.2.3 +static3==0.7.0 svg2rlg==0.3 yolk==0.4.3 -z3c.rml==2.8.1 -zope.event==4.0.3 -zope.interface==4.1.2 +z3c.rml==3.2.0 +zope.event==4.2.0 +zope.interface==4.4.0 zope.schema==4.4.2