Compare commits

...

97 Commits

Author SHA1 Message Date
David Taylor
ee948381b1 Initial work on subhire section 2016-06-16 16:22:24 +01:00
David Taylor
e1578eb0d4 Fixed responsive table fail 2016-06-15 13:00:14 +01:00
David Taylor
a7247c273e Fixed #245 2016-06-14 19:50:35 +01:00
David Taylor
f265da2f1d Fixed #241 and #244 2016-06-14 19:45:45 +01:00
David Taylor
1163b117e4 Fixed #240 2016-06-14 19:23:40 +01:00
David Taylor
f92f418bc5 Fixed waiting invoice counter - closes #239 2016-06-06 23:06:30 +01:00
David Taylor
9108cb3c4e Fail! Hide non-rigs from waiting invoices 2016-06-04 18:08:43 +01:00
David Taylor
08d17adc8a Merge branch 'develop' 2016-06-04 17:55:23 +01:00
David Taylor
68b35c2d24 Removed print button conditions following discussion 2016-05-29 23:47:56 +01:00
David Taylor
5cc69cbb41 Stopped things opening in a new window, because it's really annoying. If you want to do this, use the appropriate keyboard shortcut or mouse button 2016-05-29 23:03:41 +01:00
David Taylor
a48afb9157 Added internal/external indicators to invoice lists 2016-05-29 22:56:58 +01:00
David Taylor
3ccbdff737 Added balance to invoice page - closes #235 2016-05-29 22:51:41 +01:00
David Taylor
0990f0bfbb Only display invoice print button for external clients 2016-05-29 22:46:49 +01:00
David Taylor
f43635ee89 Added more useful information to the invoice tables 2016-05-29 22:42:17 +01:00
David Taylor
705f1bda2b Fix the query for the 'outstanding' invoices page. Previously this only displayed events with an end date set. 2016-05-29 22:05:53 +01:00
David Taylor
0ff0d06eaf Fix counter on outstanding invoices page ('length' property doesn't work because of the custom SQL query) 2016-05-29 22:04:48 +01:00
David Taylor
a769486c9c Changed order of invoice menu items to make it more intuitive (now in order of workflow) 2016-05-29 21:37:14 +01:00
David Taylor
83302c4439 Invoice UI improvements. Renamed pages, added description, and added total number of events 2016-05-29 21:30:05 +01:00
David Taylor
ba020b43f1 Merge branch 'master' into develop 2016-05-29 20:28:52 +01:00
David Taylor
eaf5c9687e Fixed typo, closes #174 2016-05-29 20:21:23 +01:00
David Taylor
a725ef5caf Removed add to google calendar link, closes #237 2016-05-29 17:09:52 +01:00
David Taylor
aa79f3628e Only redirect to HTTPS in production 2016-05-28 15:27:38 +01:00
David Taylor
000351d884 Redirect all requests to https 2016-05-28 15:20:15 +01:00
David Taylor
db58c113aa Changed font to load over https - #236 2016-05-28 14:52:48 +01:00
Tom Price
7cb8503164 Merged feature/invoice-total into develop 2016-05-24 18:44:27 +01:00
Tom Price
cc2450ff87 Make total conditional if defined 2016-05-24 17:50:48 +01:00
Tom Price
6b77393414 Fix for incorrect selection of active invoices.
Make sure waiting shows total across all pages.
2016-05-24 17:49:44 +01:00
Tom Price
7ccc8faf20 Add total for waiting events 2016-05-24 17:32:58 +01:00
Tom Price
6030288956 Cheap and dirty active totals 2016-05-24 17:17:52 +01:00
Tom Price
e4a955f323 Reformat code to PEP8 2016-05-23 12:36:21 +01:00
Tom Price
e286d8bdee Merge pull request #229 from nottinghamtec/develop 2016-05-23 00:38:31 +01:00
David Taylor
1faf8f95c8 Fixed colour coding on invoice waiting page 2016-05-22 23:13:23 +01:00
Tom Price
2913b254b4 Fix an overflow issue with dropdown fields. Closes #204 2016-05-19 14:33:33 +01:00
Tom Price
ef81536066 Add tooltips to rig type selectors. Closes #227 2016-05-19 14:21:21 +01:00
David Taylor
29aa13316d Fix for versioning when event has been deleted, closes #226 2016-04-27 20:49:42 +01:00
David Taylor
98f28aaafd Based on has name as well as MIC - Fix #224 2016-04-12 20:15:13 +01:00
David Taylor
f8a2a7a959 Made items display in version history again 2016-04-07 09:23:35 +01:00
David Taylor
767b5512e3 Fail, missed a change, actually closes #185 now 2016-04-07 00:53:57 +01:00
David Taylor
f1bd1ca674 Make 'created' noun class-specific. Closes #185 2016-04-07 00:52:33 +01:00
David Taylor
b47cfed5a9 Fixed versioning UI for revisions containing multiple event versions, hopefully 2016-04-07 00:19:18 +01:00
davidtaylorhq
71b69aa0f6 Merge pull request #220 from nottinghamtec/merge-interface
Merge interface
2016-04-06 22:08:32 +01:00
Tom Price
df61225b73 Optimise imports
So many unused imports, these have been banished
2016-04-06 21:57:54 +01:00
Tom Price
823db68a6a PEP8 format files 2016-04-06 21:53:38 +01:00
Tom Price
ebe08c3bf1 Update tests to check items that aren't selected aren't affected.
PEP8 format the file
2016-04-06 21:52:25 +01:00
David Taylor
44ccead0a4 Added merge tests
I feel like it should be possible to abstract some of these so that they're not copied out for each model, but not sure how to go about it...
2016-04-05 15:32:13 +01:00
David Taylor
99dfdcd253 Make confirmation more useful 2016-04-05 12:53:04 +01:00
David Taylor
03ca65602f Allow sorting by number of events 2016-04-05 12:08:19 +01:00
David Taylor
ca6cddb392 Add comments display to versioning history (because why not).
Maybe in future we could have a box people can type in before they save changes to an event... But that's a separate project
2016-04-05 11:50:34 +01:00
David Taylor
33ce4b622d Fixed bug with versioning interface when related objects are deleted 2016-04-05 04:18:53 +01:00
David Taylor
46434977fb Created merge admin action for Person, Venue and Organisation models. Added template. 2016-04-05 04:18:19 +01:00
davidtaylorhq
f13303490a Merge pull request #211 from nottinghamtec/wercker
Wercker
2016-03-24 12:34:00 +00:00
David Taylor
84b0a57e14 Fixed line breaks for new items. Closes #212 2016-03-24 12:22:37 +00:00
Tom Price
2ee7c064af Add a dirty work around for wercker not quite working correctly. 2016-03-18 16:48:43 +00:00
Tom Price
b2b8546e3c Add test for events_in_bounds 2016-03-18 16:41:13 +00:00
Tom Price
2945a607bf Add more tests for models.Event properties 2016-03-18 16:27:20 +00:00
Tom Price
4370cf60d1 And yet some more waits 2016-03-17 18:44:03 +00:00
Tom Price
e7496a9d82 Add some more waits for animations in failing test 2016-03-17 18:37:15 +00:00
Tom Price
77f7a25fa8 Try maximising firefox so it is large enough to pass the test 2016-03-17 18:06:37 +00:00
Tom Price
10c5ea3e7c Add a wait for the animation to complete during event creation. 2016-03-17 17:47:02 +00:00
Tom Price
cd82712742 Merge pull request #207 from nottinghamtec/fixed_jquery
Fixed jquery version.

Closes #192
2016-03-17 17:34:58 +00:00
Tom Price
3067dcdda5 Update wercker in README.md
Only show builds from master

[ci skip]
2016-03-17 17:31:14 +00:00
Tom Price
94679d6783 Update seleium version for Wercker 2016-03-17 17:23:21 +00:00
Tom Price
54dc29b4b2 Switch to jquery CDN who provide a sha256 hash to validate against.
Advise is now to always use HTTPS for libraries as somebody else manages the certificate it will always validate and it makes sure that a large target doesn't get subject to MITM attack.
2016-03-17 17:18:42 +00:00
Tom Price
e699826ce9 Merge branch 'master' into fixed_jquery 2016-03-17 17:10:35 +00:00
David Taylor
33fa19c15e Merge branch 'version_history_diff', second time lucky 2016-03-16 15:21:53 +00:00
David Taylor
d6e9b030fd Merge branch 'master' into version_history_diff 2016-03-16 15:21:13 +00:00
David Taylor
f0a3c968a7 Added unicode support 2016-03-16 15:15:12 +00:00
David Taylor
d8760a00ba Revert "Merge branch 'version_history_diff'"
This reverts commit 8ee43ef3ab, reversing
changes made to 9964d33cc0.
2016-03-16 14:54:09 +00:00
David Taylor
8ee43ef3ab Merge branch 'version_history_diff' 2016-03-16 14:37:09 +00:00
David Taylor
1b6ce32deb Merge branch 'master' into version_history_diff 2016-03-16 14:36:24 +00:00
Tom Price
9964d33cc0 Update wercker in README.md
[ci skip]
2016-03-16 14:32:38 +00:00
David Taylor
1e81b718f6 Merge branch 'deleting_duplicates' 2016-03-16 13:09:02 +00:00
David Taylor
551737b882 Merge branch 'master' into deleting_duplicates 2016-03-16 13:05:54 +00:00
David Taylor
77dd36d4b4 Merge branch 'web_calendar' 2016-03-16 12:55:26 +00:00
David Taylor
9bace9a6da Merge branch 'master' into web_calendar 2016-03-16 12:52:57 +00:00
David Taylor
c6b45da72c Made tests more reliable - wait for success page to load before checking item exists in database 2016-03-16 12:49:42 +00:00
David Taylor
0721d61bef Merge branch 'master' into web_calendar 2016-03-16 12:30:22 +00:00
David Taylor
56bc084e60 Made interface more compact 2016-03-16 12:28:37 +00:00
David Taylor
481f56a4bd Added highlighting styles 2016-03-16 12:17:26 +00:00
David Taylor
11e9931438 Fixed compass config 2016-03-16 12:16:57 +00:00
David Taylor
ea840eb43c Added diff-match-patch to requirements 2016-03-16 12:08:41 +00:00
David Taylor
2ed0a4bcf9 Updated changes view to use diff where appropriate. 2016-03-16 10:08:26 +00:00
Harry Bridge
5d48d75f34 Make protocol-agnostic 2016-03-12 18:17:03 +00:00
Harry Bridge
92beb8bf79 Fixed jquery version 2016-03-12 18:12:39 +00:00
David Taylor
ed552b402a Based on event with no MIC - closes #163 2016-03-10 11:56:09 +00:00
David Taylor
8279bec4bf Removed warning message at top - closes #203 2016-03-10 11:46:28 +00:00
David Taylor
67b2bc6d54 Applied migration to development database 2016-02-29 20:44:09 +00:00
David Taylor
e3adfecd17 Added database migration 2016-02-29 20:43:11 +00:00
David Taylor
1681ab8fee Allowed linking to specific views/dates on the calendar - closes #153 2016-02-29 20:35:53 +00:00
David Taylor
a77bc65d7b Changed delete condition to SET_NULL - closes #199 2016-02-29 20:12:41 +00:00
David Taylor
73517ed443 Revision history link on non-rigs - closes #186 2016-02-29 19:55:04 +00:00
David Taylor
f59e11ecc4 Updated VAT Registration Number - Closes #197 2016-02-29 19:37:59 +00:00
Tom Price
19f619b2d4 Merge pull request #194 from nottinghamtec/login-autofocus
Made username field autofocus (using HTML5 Attribute). Fixes #193
2016-02-12 16:46:10 +00:00
David Taylor
e0d03c2cc3 Made username field autofocus (using HTML5 Attribute). Fixes #193 2016-02-12 16:27:11 +00:00
Tom Price
69f7152dd6 Merge pull request #191 from samozzy/patch-1
Update konami.js
2016-02-01 17:11:20 +00:00
samozzy
581f28757a Update konami.js
Disable Weirdness Factor 12 on devices without an 'escape' key.
2016-02-01 17:10:06 +00:00
Tom Price
660a54d955 Merge pull request #177 from nottinghamtec/login
Move captcha from login screen.

Close #176
2016-01-20 13:45:26 +00:00
46 changed files with 1252 additions and 548 deletions

View File

@@ -58,7 +58,7 @@ def api_key_required(function):
try: try:
user_object = models.Profile.objects.get(pk=userid) user_object = models.Profile.objects.get(pk=userid)
except Profile.DoesNotExist: except models.Profile.DoesNotExist:
return error_resp return error_resp
if user_object.api_key != key: if user_object.api_key != key:

View File

@@ -12,8 +12,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
import os import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__)) BASE_DIR = os.path.dirname(os.path.dirname(__file__))
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
@@ -27,6 +25,10 @@ TEMPLATE_DEBUG = True
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com'] ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
if not DEBUG:
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
INTERNAL_IPS = ['127.0.0.1'] INTERNAL_IPS = ['127.0.0.1']
ADMINS = ( ADMINS = (
@@ -44,6 +46,7 @@ INSTALLED_APPS = (
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'RIGS', 'RIGS',
'subhire',
'debug_toolbar', 'debug_toolbar',
'registration', 'registration',
@@ -55,6 +58,7 @@ INSTALLED_APPS = (
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware', 'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware',
'reversion.middleware.RevisionMiddleware', 'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',

View File

@@ -1,5 +1,5 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
[![wercker status](https://app.wercker.com/status/b26100ecccdfb46a9a9056553daac5b7/m/master "wercker status")](https://app.wercker.com/project/bykey/b26100ecccdfb46a9a9056553daac5b7) [![wercker status](https://app.wercker.com/status/2dbe0517c3d83859c985ffc5a55a2802/m/master "wercker status")](https://app.wercker.com/project/bykey/2dbe0517c3d83859c985ffc5a55a2802)
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. 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.
@@ -75,4 +75,4 @@ python manage.py runserver
Please refer to Django documentation for a full list of options available here. Please refer to Django documentation for a full list of options available here.
### Committing, pushing and testing ### ### Committing, pushing and testing ###
Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person. Feel free to commit as you wish, on your own branch. On my branch (master for development) do not commit code that you either know doesn't work or don't know works. If you must commit this code, please make sure you say in the commit message that it isn't working, and if you can why it isn't working. If and only if you absolutely must push, then please don't leave it as the HEAD for too long, it's not much to ask but when you are done just make sure you haven't broken the HEAD for the next person.

View File

@@ -4,25 +4,32 @@ from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import reversion import reversion
from django.contrib.admin import helpers
from django.template.response import TemplateResponse
from django.contrib import messages
from django.db import transaction
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Count
from django.forms import ModelForm
# Register your models here. # Register your models here.
admin.site.register(models.Person, reversion.VersionAdmin)
admin.site.register(models.Organisation, reversion.VersionAdmin)
admin.site.register(models.VatRate, reversion.VersionAdmin) admin.site.register(models.VatRate, reversion.VersionAdmin)
admin.site.register(models.Venue, reversion.VersionAdmin)
admin.site.register(models.Event, reversion.VersionAdmin) admin.site.register(models.Event, reversion.VersionAdmin)
admin.site.register(models.EventItem, reversion.VersionAdmin) admin.site.register(models.EventItem, reversion.VersionAdmin)
admin.site.register(models.Invoice) admin.site.register(models.Invoice)
admin.site.register(models.Payment) admin.site.register(models.Payment)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin): class ProfileAdmin(UserAdmin):
fieldsets = ( fieldsets = (
(None, {'fields': ('username', 'password')}), (None, {'fields': ('username', 'password')}),
(_('Personal info'), { (_('Personal info'), {
'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}), 'fields': ('first_name', 'last_name', 'email', 'initials', 'phone')}),
(_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', (_('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser',
'groups', 'user_permissions')}), 'groups', 'user_permissions')}),
(_('Important dates'), { (_('Important dates'), {
'fields': ('last_login', 'date_joined')}), 'fields': ('last_login', 'date_joined')}),
) )
add_fieldsets = ( add_fieldsets = (
(None, { (None, {
@@ -33,4 +40,76 @@ class ProfileAdmin(UserAdmin):
form = forms.ProfileChangeForm form = forms.ProfileChangeForm
add_form = forms.ProfileCreationForm add_form = forms.ProfileCreationForm
admin.site.register(models.Profile, ProfileAdmin)
class AssociateAdmin(reversion.VersionAdmin):
list_display = ('id', 'name', 'number_of_events')
search_fields = ['id', 'name']
list_display_links = ['id', 'name']
actions = ['merge']
merge_fields = ['name']
def get_queryset(self, request):
return super(AssociateAdmin, self).get_queryset(request).annotate(event_count=Count('event'))
def number_of_events(self, obj):
return obj.latest_events.count()
number_of_events.admin_order_field = 'event_count'
def merge(self, request, queryset):
if request.POST.get('post'): # Has the user confirmed which is the master record?
try:
masterObjectPk = request.POST.get('master')
masterObject = queryset.get(pk=masterObjectPk)
except ObjectDoesNotExist:
self.message_user(request, "An error occured. Did you select a 'master' record?", level=messages.ERROR)
return
with transaction.atomic(), reversion.create_revision():
for obj in queryset.exclude(pk=masterObjectPk):
events = obj.event_set.all()
for event in events:
masterObject.event_set.add(event)
obj.delete()
reversion.set_comment('Merging Objects')
self.message_user(request, "Objects successfully merged.")
return
else: # Present the confirmation screen
class TempForm(ModelForm):
class Meta:
model = queryset.model
fields = self.merge_fields
forms = []
for obj in queryset:
forms.append(TempForm(instance=obj))
context = {
'title': _("Are you sure?"),
'queryset': queryset,
'action_checkbox_name': helpers.ACTION_CHECKBOX_NAME,
'forms': forms
}
return TemplateResponse(request, 'RIGS/admin_associate_merge.html', context,
current_app=self.admin_site.name)
@admin.register(models.Person)
class PersonAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes']
@admin.register(models.Venue)
class VenueAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'three_phase_available']
@admin.register(models.Organisation)
class OrganisationAdmin(AssociateAdmin):
list_display = ('id', 'name', 'phone', 'email', 'number_of_events')
merge_fields = ['name', 'phone', 'email', 'address', 'notes', 'union_account']

View File

@@ -1,32 +1,41 @@
import cStringIO as StringIO import cStringIO as StringIO
import datetime
import re
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy from django.core.urlresolvers import reverse_lazy
from django.db import connection
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.views import generic
from django.template import RequestContext
from django.template.loader import get_template
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.contrib import messages from django.template import RequestContext
import datetime from django.template.loader import get_template
from django.views import generic
from django.db.models import Q
from z3c.rml import rml2pdf from z3c.rml import rml2pdf
from RIGS import models from RIGS import models
import re
class InvoiceIndex(generic.ListView): class InvoiceIndex(generic.ListView):
model = models.Invoice model = models.Invoice
template_name = 'RIGS/invoice_list.html' template_name = 'RIGS/invoice_list_active.html'
def get_context_data(self, **kwargs):
context = super(InvoiceIndex, self).get_context_data(**kwargs)
total = 0
for i in context['object_list']:
total += i.balance
context['total'] = total
context['count'] = len(list(context['object_list']))
return context
def get_queryset(self): def get_queryset(self):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must # Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \ sql = "SELECT * FROM " \
"(SELECT " \ "(SELECT " \
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \ "(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \ "(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" as p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \ "(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \ "\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
"AS sub " \ "AS sub " \
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \ "WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
@@ -40,6 +49,7 @@ class InvoiceIndex(generic.ListView):
class InvoiceDetail(generic.DetailView): class InvoiceDetail(generic.DetailView):
model = models.Invoice model = models.Invoice
class InvoicePrint(generic.View): class InvoicePrint(generic.View):
def get(self, request, pk): def get(self, request, pk):
invoice = get_object_or_404(models.Invoice, pk=pk) invoice = get_object_or_404(models.Invoice, pk=pk)
@@ -54,8 +64,8 @@ class InvoicePrint(generic.View):
'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF', 'bold': 'RIGS/static/fonts/OPENSANS-BOLD.TTF',
} }
}, },
'invoice':invoice, 'invoice': invoice,
'current_user':request.user, 'current_user': request.user,
}) })
rml = template.render(context) rml = template.render(context)
@@ -72,6 +82,7 @@ class InvoicePrint(generic.View):
response.write(pdfData) response.write(pdfData)
return response return response
class InvoiceVoid(generic.View): class InvoiceVoid(generic.View):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
pk = kwargs.get('pk') pk = kwargs.get('pk')
@@ -86,6 +97,7 @@ class InvoiceVoid(generic.View):
class InvoiceArchive(generic.ListView): class InvoiceArchive(generic.ListView):
model = models.Invoice model = models.Invoice
template_name = 'RIGS/invoice_list_archive.html'
paginate_by = 25 paginate_by = 25
@@ -94,14 +106,33 @@ class InvoiceWaiting(generic.ListView):
paginate_by = 25 paginate_by = 25
template_name = 'RIGS/event_invoice.html' template_name = 'RIGS/event_invoice.html'
def get_context_data(self, **kwargs):
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
total = 0
for obj in self.get_objects():
total += obj.sum_total
context['total'] = total
context['count'] = len(self.get_objects())
return context
def get_queryset(self): def get_queryset(self):
return self.get_objects()
def get_objects(self):
# @todo find a way to select items # @todo find a way to select items
events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(), events = self.model.objects.filter(
invoice__isnull=True) \ (
.order_by('start_date') \ Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
) & Q(invoice__isnull=True) # Has not already been invoiced
& Q(is_rig=True) # Is a rig (not non-rig)
).order_by('start_date') \
.select_related('person', .select_related('person',
'organisation', 'organisation',
'venue', 'mic') 'venue', 'mic') \
.prefetch_related('items')
return events return events
@@ -119,7 +150,7 @@ class InvoiceEvent(generic.View):
class PaymentCreate(generic.CreateView): class PaymentCreate(generic.CreateView):
model = models.Payment model = models.Payment
fields = ['invoice','date','amount','method'] fields = ['invoice', 'date', 'amount', 'method']
def get_initial(self): def get_initial(self):
initial = super(generic.CreateView, self).get_initial() initial = super(generic.CreateView, self).get_initial()
@@ -139,4 +170,4 @@ class PaymentDelete(generic.DeleteView):
model = models.Payment model = models.Payment
def get_success_url(self): def get_success_url(self):
return self.request.POST.get('next') return self.request.POST.get('next')

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('RIGS', '0023_auto_20150529_0048'),
]
operations = [
migrations.AlterField(
model_name='event',
name='based_on',
field=models.ForeignKey(related_name='future_events', on_delete=django.db.models.deletion.SET_NULL, blank=True, to='RIGS.Event', null=True),
),
]

View File

@@ -1,31 +1,32 @@
import datetime
import hashlib import hashlib
import datetime, pytz import pytz
from django.db import models, connection
from django.contrib.auth.models import AbstractUser
from django.conf import settings
from django.utils.functional import cached_property
from django.utils.encoding import python_2_unicode_compatible
import reversion
import string
import random import random
import string
from collections import Counter from collections import Counter
from django.core.urlresolvers import reverse_lazy
from django.core.exceptions import ValidationError
from decimal import Decimal 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. # Create your models here.
@python_2_unicode_compatible @python_2_unicode_compatible
class Profile(AbstractUser): class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False) initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True) phone = models.CharField(max_length=13, null=True, blank=True)
api_key = models.CharField(max_length=40,blank=True,editable=False, null=True) api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
@classmethod @classmethod
def make_api_key(cls): def make_api_key(cls):
size=20 size = 20
chars=string.ascii_letters + string.digits chars = string.ascii_letters + string.digits
new_api_key = ''.join(random.choice(chars) for x in range(size)) new_api_key = ''.join(random.choice(chars) for x in range(size))
return new_api_key; return new_api_key;
@@ -55,6 +56,7 @@ class Profile(AbstractUser):
('view_profile', 'Can view Profile'), ('view_profile', 'Can view Profile'),
) )
class RevisionMixin(object): class RevisionMixin(object):
@property @property
def last_edited_at(self): def last_edited_at(self):
@@ -79,10 +81,11 @@ class RevisionMixin(object):
versions = reversion.get_for_object(self) versions = reversion.get_for_object(self)
if versions: if versions:
version = reversion.get_for_object(self)[0] version = reversion.get_for_object(self)[0]
return "V{0} | R{1}".format(version.pk,version.revision.pk) return "V{0} | R{1}".format(version.pk, version.revision.pk)
else: else:
return None return None
@reversion.register @reversion.register
@python_2_unicode_compatible @python_2_unicode_compatible
class Person(models.Model, RevisionMixin): class Person(models.Model, RevisionMixin):
@@ -97,7 +100,7 @@ class Person(models.Model, RevisionMixin):
def __str__(self): def __str__(self):
string = self.name string = self.name
if self.notes is not None: if self.notes is not None:
if len(self.notes) > 0: if len(self.notes) > 0:
string += "*" string += "*"
return string return string
@@ -108,7 +111,7 @@ class Person(models.Model, RevisionMixin):
if e.organisation: if e.organisation:
o.append(e.organisation) o.append(e.organisation)
#Count up occurances and put them in descending order # Count up occurances and put them in descending order
c = Counter(o) c = Counter(o)
stats = c.most_common() stats = c.most_common()
return stats return stats
@@ -141,7 +144,7 @@ class Organisation(models.Model, RevisionMixin):
def __str__(self): def __str__(self):
string = self.name string = self.name
if self.notes is not None: if self.notes is not None:
if len(self.notes) > 0: if len(self.notes) > 0:
string += "*" string += "*"
return string return string
@@ -151,8 +154,8 @@ class Organisation(models.Model, RevisionMixin):
for e in Event.objects.filter(organisation=self).select_related('person'): for e in Event.objects.filter(organisation=self).select_related('person'):
if e.person: if e.person:
p.append(e.person) p.append(e.person)
#Count up occurances and put them in descending order # Count up occurances and put them in descending order
c = Counter(p) c = Counter(p)
stats = c.most_common() stats = c.most_common()
return stats return stats
@@ -238,12 +241,18 @@ class Venue(models.Model, RevisionMixin):
class EventManager(models.Manager): class EventManager(models.Manager):
def current_events(self): def current_events(self):
events = self.filter( 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(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after status=Event.CANCELLED)) | # Starts after with no end
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire (models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT 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 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') ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
return events return events
def events_in_bounds(self, start, end): def events_in_bounds(self, start, end):
@@ -251,15 +260,17 @@ class EventManager(models.Manager):
(models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds (models.Q(start_date__gte=start.date(), start_date__lte=end.date())) | # Start date in bounds
(models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds (models.Q(end_date__gte=start.date(), end_date__lte=end.date())) | # End date in bounds
(models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds (models.Q(access_at__gte=start, access_at__lte=end)) | # Access at in bounds
(models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds (models.Q(meet_at__gte=start, meet_at__lte=end)) | # Meet at in bounds
(models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after (models.Q(start_date__lte=start, end_date__gte=end)) | # Start before, end after
(models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after (models.Q(access_at__lte=start, start_date__gte=end)) | # Access before, start after
(models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after (models.Q(access_at__lte=start, end_date__gte=end)) | # Access before, end after
(models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after (models.Q(meet_at__lte=start, start_date__gte=end)) | # Meet before, start after
(models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after (models.Q(meet_at__lte=start, end_date__gte=end)) # Meet before, end after
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic') ).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
return events return events
def rig_count(self): def rig_count(self):
@@ -301,7 +312,8 @@ class Event(models.Model, RevisionMixin):
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL) status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False) dry_hire = models.BooleanField(default=False)
is_rig = models.BooleanField(default=True) is_rig = models.BooleanField(default=True)
based_on = models.ForeignKey('Event', related_name='future_events', blank=True, null=True) based_on = models.ForeignKey('Event', on_delete=models.SET_NULL, related_name='future_events', blank=True,
null=True)
# Timing # Timing
start_date = models.DateField() start_date = models.DateField()
@@ -327,6 +339,7 @@ class Event(models.Model, RevisionMixin):
""" """
EX Vat EX Vat
""" """
@property @property
def sum_total(self): def sum_total(self):
# Manual querying is required for efficiency whilst maintaining floating point arithmetic # Manual querying is required for efficiency whilst maintaining floating point arithmetic
@@ -334,14 +347,15 @@ class Event(models.Model, RevisionMixin):
# sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id # sql = "SELECT SUM(quantity * cost) AS sum_total FROM \"RIGS_eventitem\" WHERE event_id=%i" % self.id
# else: # else:
# sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id # sql = "SELECT id, SUM(quantity * cost) AS sum_total FROM RIGS_eventitem WHERE event_id=%i" % self.id
#total = self.items.raw(sql)[0] # total = self.items.raw(sql)[0]
#if total.sum_total: # if total.sum_total:
# return total.sum_total # return total.sum_total
#total = 0.0 # total = 0.0
#for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"): # for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
# total += item.sum # total += item.sum
total = EventItem.objects.filter(event=self).aggregate( total = EventItem.objects.filter(event=self).aggregate(
sum_total=models.Sum(models.F('cost')*models.F('quantity'), output_field=models.DecimalField(max_digits=10, decimal_places=2)) sum_total=models.Sum(models.F('cost') * models.F('quantity'),
output_field=models.DecimalField(max_digits=10, decimal_places=2))
)['sum_total'] )['sum_total']
if total: if total:
return total return total
@@ -358,6 +372,7 @@ class Event(models.Model, RevisionMixin):
""" """
Inc VAT Inc VAT
""" """
@property @property
def total(self): def total(self):
return self.sum_total + self.vat return self.sum_total + self.vat
@@ -382,7 +397,7 @@ class Event(models.Model, RevisionMixin):
def earliest_time(self): def earliest_time(self):
"""Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object""" """Finds the earliest time defined in the event - this function could return either a tzaware datetime, or a naiive date object"""
#Put all the datetimes in a list # Put all the datetimes in a list
datetime_list = [] datetime_list = []
if self.access_at: if self.access_at:
@@ -394,22 +409,22 @@ class Event(models.Model, RevisionMixin):
# If there is no start time defined, pretend it's midnight # If there is no start time defined, pretend it's midnight
startTimeFaked = False startTimeFaked = False
if self.has_start_time: if self.has_start_time:
startDateTime = datetime.datetime.combine(self.start_date,self.start_time) startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
else: else:
startDateTime = datetime.datetime.combine(self.start_date,datetime.time(00,00)) startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
startTimeFaked = True startTimeFaked = True
#timezoneIssues - apply the default timezone to the naiive datetime # timezoneIssues - apply the default timezone to the naiive datetime
tz = pytz.timezone(settings.TIME_ZONE) tz = pytz.timezone(settings.TIME_ZONE)
startDateTime = tz.localize(startDateTime) startDateTime = tz.localize(startDateTime)
datetime_list.append(startDateTime) # then add it to the list datetime_list.append(startDateTime) # then add it to the list
earliest = min(datetime_list).astimezone(tz) #find the earliest datetime in the list earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
# if we faked it & it's the earliest, better own up # if we faked it & it's the earliest, better own up
if startTimeFaked and earliest==startDateTime: if startTimeFaked and earliest == startDateTime:
return self.start_date return self.start_date
return earliest return earliest
@property @property
@@ -421,7 +436,7 @@ class Event(models.Model, RevisionMixin):
endDate = self.start_date endDate = self.start_date
if self.has_end_time: if self.has_end_time:
endDateTime = datetime.datetime.combine(endDate,self.end_time) endDateTime = datetime.datetime.combine(endDate, self.end_time)
tz = pytz.timezone(settings.TIME_ZONE) tz = pytz.timezone(settings.TIME_ZONE)
endDateTime = tz.localize(endDateTime) endDateTime = tz.localize(endDateTime)
@@ -430,7 +445,6 @@ class Event(models.Model, RevisionMixin):
else: else:
return endDate return endDate
objects = EventManager() objects = EventManager()
def get_absolute_url(self): def get_absolute_url(self):
@@ -446,7 +460,7 @@ class Event(models.Model, RevisionMixin):
startEndSameDay = not self.end_date or self.end_date == self.start_date startEndSameDay = not self.end_date or self.end_date == self.start_date
hasStartAndEnd = self.has_start_time and self.has_end_time hasStartAndEnd = self.has_start_time and self.has_end_time
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time: if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.') raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Call :meth:`full_clean` before saving.""" """Call :meth:`full_clean` before saving."""
@@ -503,15 +517,6 @@ class Invoice(models.Model):
@property @property
def payment_total(self): def payment_total(self):
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
#if connection.vendor == 'postgresql':
# sql = "SELECT SUM(amount) AS total FROM \"RIGS_payment\" WHERE invoice_id=%i" % self.id
#else:
# sql = "SELECT id, SUM(amount) AS total FROM RIGS_payment WHERE invoice_id=%i" % self.id
#total = self.payment_set.raw(sql)[0]
#if total.total:
# return total.total
#return 0.0
total = self.payment_set.aggregate(total=models.Sum('amount'))['total'] total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
if total: if total:
return total return total
@@ -552,4 +557,4 @@ class Payment(models.Model):
method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True) method = models.CharField(max_length=2, choices=METHODS, null=True, blank=True)
def __str__(self): def __str__(self):
return "%s: %d" % (self.get_method_display(), self.amount) return "%s: %d" % (self.get_method_display(), self.amount)

View File

@@ -37,6 +37,12 @@ class RigboardIndex(generic.TemplateView):
class WebCalendar(generic.TemplateView): class WebCalendar(generic.TemplateView):
template_name = 'RIGS/calendar.html' template_name = 'RIGS/calendar.html'
def get_context_data(self, **kwargs):
context = super(WebCalendar, self).get_context_data(**kwargs)
context['view'] = kwargs.get('view','')
context['date'] = kwargs.get('date','')
return context
class EventDetail(generic.DetailView): class EventDetail(generic.DetailView):
model = models.Event model = models.Event

View File

@@ -11,6 +11,7 @@ fonts_dir = "fonts"
# You can select your preferred output style here (can be overridden via the command line): # You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed # output_style = :expanded or :nested or :compact or :compressed
output_style = :compressed
# To enable relative paths to assets via compass helper functions. Uncomment: # To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true # relative_assets = true

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -39,7 +39,7 @@ var Konami = function (callback) {
return false; return false;
} }
}, this); }, this);
this.iphone.load(link); /*this.iphone.load(link);*/
}, },
code: function (link) { code: function (link) {
window.location = link window.location = link

View File

@@ -75,6 +75,16 @@ textarea {
} }
} }
del {
background-color: #f2dede;
border-radius: 3px;
}
ins {
background-color: #dff0d8;
border-radius: 3px;
}
.loading-animation { .loading-animation {
position: relative; position: relative;
margin: 30px auto 0; margin: 30px auto 0;

View File

@@ -40,6 +40,9 @@
{% endif %} {% endif %}
{% include 'RIGS/object_button.html' with object=version.new %} {% include 'RIGS/object_button.html' with object=version.new %}
{% if version.revision.comment %}
({{ version.revision.comment }})
{% endif %}
</small> </small>
</p> </p>

View File

@@ -59,6 +59,7 @@
<td>Version ID</td> <td>Version ID</td>
<td>User</td> <td>User</td>
<td>Changes</td> <td>Changes</td>
<td>Comment</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -71,10 +72,11 @@
<td>{{ version.revision.user.name }}</td> <td>{{ version.revision.user.name }}</td>
<td> <td>
{% if version.old == None %} {% if version.old == None %}
Object Created {{version.new|to_class_name}} Created
{% else %} {% else %}
{% include 'RIGS/version_changes.html' %} {% include 'RIGS/version_changes.html' %}
{% endif %} </td> {% endif %} </td>
<td>{{ version.revision.comment }}</td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,40 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% block content %}
<form action="" method="post">{% csrf_token %}
<p>The following objects will be merged. Please select the 'master' record which you would like to keep. Other records will have associated events moved to the 'master' copy, and then will be deleted.</p>
<table>
{% for form in forms %}
{% if forloop.first %}
<tr>
<th></th>
<th> ID </th>
{% for field in form %}
<th>{{ field.label }}</th>
{% endfor %}
</tr>
{% endif %}
<tr>
<td><input type="radio" name="master" value="{{form.instance.pk|unlocalize}}"></td>
<td>{{form.instance.pk}}</td>
{% for field in form %}
<td> {{ field.value }} </td>
{% endfor %}
</tr>
{% endfor %}
</table>
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="merge" />
<input type="hidden" name="post" value="yes" />
<input type="submit" value="Merge them" />
</div>
</form>
{% endblock %}

View File

@@ -14,8 +14,28 @@
<script src="{% static "js/moment.min.js" %}"></script> <script src="{% static "js/moment.min.js" %}"></script>
<script src="{% static "js/fullcalendar.js" %}"></script> <script src="{% static "js/fullcalendar.js" %}"></script>
<script> <script>
function getUrlVars() {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m,key,value) {
vars[key] = value;
});
return vars;
}
$(document).ready(function() { $(document).ready(function() {
viewToUrl = {
'agendaWeek':'week',
'agendaDay':'day',
'month':'month'
}
viewFromUrl = {
'week':'agendaWeek',
'day':'agendaDay',
'month':'month'
}
$('#calendar').fullCalendar({ $('#calendar').fullCalendar({
editable: false, editable: false,
eventLimit: true, // allow "more" link when too many events eventLimit: true, // allow "more" link when too many events
@@ -114,8 +134,11 @@
$('#day-button').addClass('active'); $('#day-button').addClass('active');
break; break;
} }
history.replaceState(null,null,'{% url 'web_calendar' %}'+viewToUrl[view.name]+'/'+view.intervalStart.format('YYYY-MM-DD')+'/');
} }
}); });
// set some button listeners // set some button listeners
@@ -146,6 +169,18 @@
} }
}); });
// Go to the initial settings, if they're valid
view = viewFromUrl['{{view}}'];
$('#calendar').fullCalendar( 'changeView', view);
day = moment('{{date}}');
if(day.isValid()){
$('#calendar').fullCalendar( 'gotoDate', day);
}else{
console.log('Supplied date is invalid - using default')
}
}); });
</script> </script>

View File

@@ -151,7 +151,7 @@
<a href="{% url 'event_detail' pk=object.based_on.pk %}"> <a href="{% url 'event_detail' pk=object.based_on.pk %}">
{% if object.based_on.is_rig %}N{{ object.based_on.pk|stringformat:"05d" }}{% else %} {% if object.based_on.is_rig %}N{{ object.based_on.pk|stringformat:"05d" }}{% else %}
{{ object.based_on.pk }}{% endif %} {{ object.based_on.pk }}{% endif %}
{{ object.base_on.name }} by {{ object.based_on.mic.name }} {{ object.based_on.name }} {% if object.based_on.mic %}by {{ object.based_on.mic.name }}{% endif %}
</a> </a>
{% endif %} {% endif %}
</dd> </dd>
@@ -233,13 +233,17 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
</div>
{% endif %}
{% endif %}
{% if not request.is_ajax %}
<div class="col-sm-12 text-right">
<div> <div>
<a href="{% url 'event_history' object.pk %}" title="View Revision History"> <a href="{% url 'event_history' object.pk %}" title="View Revision History">
Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }} Last edited at {{ object.last_edited_at }} by {{ object.last_edited_by.name }}
</a> </a>
</div> </div>
</div> </div>
{% endif %}
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -29,9 +29,9 @@
} }
function setTime02Hours() { function setTime02Hours() {
var id_start = "{{ form.start_date.id_for_label }}" var id_start = "{{ form.start_date.id_for_label }}";
var id_end_date = "{{ form.end_date.id_for_label }}" var id_end_date = "{{ form.end_date.id_for_label }}";
var id_end_time = "{{ form.end_time.id_for_label }}" var id_end_time = "{{ form.end_time.id_for_label }}";
if ($('#'+id_start).val() == $('#'+id_end_date).val()) { if ($('#'+id_start).val() == $('#'+id_end_date).val()) {
var end_date = new Date($('#'+id_end_date).val()); var end_date = new Date($('#'+id_end_date).val());
end_date.setDate(end_date.getDate() + 1); end_date.setDate(end_date.getDate() + 1);
@@ -63,11 +63,12 @@
} else { } else {
$('.form-is_rig').slideDown(); $('.form-is_rig').slideDown();
} }
$('.form-hws').css('overflow', 'visible');
} else { } else {
$('#{{form.is_rig.auto_id}}').prop('checked', false); $('#{{form.is_rig.auto_id}}').prop('checked', false);
$('.form-is_rig').slideUp(); $('.form-is_rig').slideUp();
} }
}) });
{% endif %} {% endif %}
function supportsDate() { function supportsDate() {
@@ -106,7 +107,7 @@
}); });
} }
}) });
$(document).ready(function () { $(document).ready(function () {
setupItemTable($("#{{ form.items_json.id_for_label }}").val()); setupItemTable($("#{{ form.items_json.id_for_label }}").val());
@@ -150,10 +151,12 @@
<div class="col-md-12 well"> <div class="col-md-12 well">
<div class="form-group" id="is_rig-selector"> <div class="form-group" id="is_rig-selector">
<div class="col-sm-12"> <div class="col-sm-12">
<span class="col-sm-6"> <span class="col-sm-6" data-toggle="tooltip"
title="Anything that involves TEC kit, crew, or otherwise us providing a service to anyone.">
<button type="button" class="btn btn-primary col-xs-12" data-is_rig="1">Rig</button> <button type="button" class="btn btn-primary col-xs-12" data-is_rig="1">Rig</button>
</span> </span>
<span class="col-sm-6"> <span class="col-sm-6" data-toggle="tooltip"
title="Things that aren't service-based, like training, meetings and site visits.">
<button type="button" class="btn btn-info col-xs-12" data-is_rig="0">Non-Rig</button> <button type="button" class="btn btn-info col-xs-12" data-is_rig="0">Non-Rig</button>
</span> </span>
</div> </div>

View File

@@ -1,66 +1,91 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load paginator from filters %} {% load paginator from filters %}
{% load static %}
{% block title %}Events for Invoice{% endblock %} {% block title %}Events for Invoice{% endblock %}
{% block js %}
<script src="{% static "js/tooltip.js" %}"></script>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
{% endblock %}
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<h2>Events for Invoice</h2> <h2>Events for Invoice ({{count}} Events, £ {{ total|floatformat:2 }})</h2>
<p>These events have happened, but paperwork has not yet been sent to treasury</p>
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}
</div> </div>
{% endif %} {% endif %}
<table class="table table-responsive table-hover"> <div class="table-responsive col-sm-12">
<thead> <table class="table table-hover">
<tr> <thead>
<th class="hiddenx-xs">#</th> <tr>
<th>Date</th> <th>#</th>
<th>Event</th> <th>Start Date</th>
<th>Client</th> <th>Event Name</th>
<th>Cost</th> <th>Client</th>
<th class="hidden-xs">MIC</th> <th>Cost</th>
<th></th> <th>MIC</th>
</tr> <th></th>
</thead>
<tbody>
{% for object in object_list %}
<tr class="
{% if event.cancelled %}
active
{% elif event.confirmed and event.mic or not event.is_rig %}
{# interpreated as (booked and mic) or is non rig #}
success
{% elif event.mic %}
warning
{% else %}
danger
{% endif %}
">
<td class="hidden-xs"><a href="{% url 'event_detail' object.pk %}" target="_blank">N{{ object.pk|stringformat:"05d" }}</a></td>
<td>{{ object.end_date }}</td>
<td>{{ object.name }}</td>
<td>
{% if object.organisation %}
{{ object.organisation.name }}
{% else %}
{{ object.person.name }}
{% endif %}
</td>
<td>{{ object.sum_total|floatformat:2 }}</td>
<td class="text-center">
{{ object.mic.initials }}<br/>
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
</td>
<td class="text-right">
<a href="{% url 'invoice_event' object.pk %}" target="_blank" class="btn btn-default">
<span class="glyphicon glyphicon-gbp"></span>
</a>
</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for object in object_list %}
<tr class="
{% if object.cancelled %}
active text-muted
{% elif not object.is_rig %}
info
{% elif object.confirmed and object.mic %}
{# interpreated as (booked and mic) #}
success
{% elif object.mic %}
warning
{% else %}
danger
{% endif %}
">
<td><a href="{% url 'event_detail' object.pk %}">N{{ object.pk|stringformat:"05d" }}</a><br>
<span class="text-muted">{{ object.get_status_display }}</span></td>
<td>{{ object.start_date }}</td>
<td>{{ object.name }}</td>
<td>
{% if object.organisation %}
{{ object.organisation.name }}
<br>
<span class="text-muted">{{ object.organisation.union_account|yesno:'Internal,External' }}</span>
{% else %}
{{ object.person.name }}
<br>
<span class="text-muted">External</span>
{% endif %}
</td>
<td>{{ object.sum_total|floatformat:2 }}</td>
<td class="text-center">
{% if object.mic %}
{{ object.mic.initials }}<br>
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo"/>
{% else %}
<span class="glyphicon glyphicon-exclamation-sign"></span>
{% endif %}
</td>
<td class="text-right">
<a href="{% url 'invoice_event' object.pk %}" class="btn btn-default" data-toggle="tooltip" title="'Invoice' this event - click this when paperwork has been sent to treasury">
<span class="glyphicon glyphicon-gbp"></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}

View File

@@ -189,7 +189,7 @@
<keepTogether> <keepTogether>
<blockTable style="totalTable" colWidths="300,115,80"> <blockTable style="totalTable" colWidths="300,115,80">
<tr> <tr>
<td>{% if not invoice %}VAT Registration Number: 116252989{% endif %}</td> <td>{% if not invoice %}VAT Registration Number: 170734807{% endif %}</td>
<td>Total (ex. VAT)</td> <td>Total (ex. VAT)</td>
<td>£ {{ object.sum_total|floatformat:2 }}</td> <td>£ {{ object.sum_total|floatformat:2 }}</td>
</tr> </tr>
@@ -209,7 +209,7 @@
<para> <para>
{% if invoice %} {% if invoice %}
VAT Registration Number: 116252989 VAT Registration Number: 170734807
{% else %} {% else %}
<b>This contract is not an invoice.</b> <b>This contract is not an invoice.</b>
{% endif %} {% endif %}

View File

@@ -1,7 +1,7 @@
<div class="table-responsive"> <div class="table-responsive">
<table class="table"> <table class="table">
<thead> <thead>
<td class="hidden-xs">#</td> <td>#</td>
<td>Event Date</td> <td>Event Date</td>
<td>Event Details</td> <td>Event Details</td>
<td>Event Timings</td> <td>Event Timings</td>
@@ -23,7 +23,7 @@
danger danger
{% endif %} {% endif %}
"> ">
<td class="hidden-xs">{{ event.pk }}</td> <td>{{ event.pk }}</td>
<td> <td>
<div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div> <div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div>
{% if event.end_date and event.end_date != event.start_date %} {% if event.end_date and event.end_date != event.start_date %}

View File

@@ -109,6 +109,11 @@
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr>
<td class="text-right"><strong>Balance:</strong></td>
<td>{{ object.balance|floatformat:2 }}</td>
<td></td>
<td></td>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@@ -5,38 +5,56 @@
{% block content %} {% block content %}
<div class="col-sm-12"> <div class="col-sm-12">
<h2>Invoices</h2> <h2>{% block heading %}Invoices{% endblock %}</h2>
{% block description %}{% endblock %}
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}
</div> </div>
{% endif %} {% endif %}
<table class="table table-responsive table-hover"> <div class="table-responsive col-sm-12">
<thead> <table class="table table-hover">
<tr> <thead>
<th>#</th> <tr>
<th>Event</th> <th>#</th>
<th>Invoice Date</th> <th>Event</th>
<th>Balance</th> <th>Client</th>
<th></th> <th>Event Date</th>
</tr> <th>Invoice Date</th>
</thead> <th>Balance</th>
<tbody> <th></th>
{% for object in object_list %}
<tr class="{% if object.void %}danger{% elif object.balance == 0 %}success{% endif %}">
<td>{{ object.pk }}</td>
<td><a href="{% url 'event_detail' object.event.pk %}" target="_blank">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }}</td>
<td>{{ object.invoice_date }}</td>
<td>{{ object.balance|floatformat:2 }}</td>
<td class="text-right">
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-pencil"></span>
</a>
</td>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for object in object_list %}
<tr class="{% if object.void %}danger{% elif object.balance == 0 %}success{% endif %}">
<td>{{ object.pk }}</td>
<td><a href="{% url 'event_detail' object.event.pk %}">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }} <br>
<span class="text-muted">{{ object.event.get_status_display }}</span></td>
</td>
<td>{% if object.event.organisation %}
{{ object.event.organisation.name }}
<br>
<span class="text-muted">{{ object.event.organisation.union_account|yesno:'Internal,External' }}</span>
{% else %}
{{ object.event.person.name }}
<br>
<span class="text-muted">External</span>
{% endif %}
</td>
<td>{{ object.event.start_date }}</td>
<td>{{ object.invoice_date }}</td>
<td>{{ object.balance|floatformat:2 }}</td>
<td class="text-right">
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default">
<span class="glyphicon glyphicon-pencil"></span>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if is_paginated %} {% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right"> <div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %} {% paginator %}

View File

@@ -0,0 +1,13 @@
{% extends 'RIGS/invoice_list.html' %}
{% block title %}
Outstanding Invoices
{% endblock %}
{% block heading %}
Outstanding Invoices ({{ count }} Events, £ {{ total|floatformat:2 }})
{% endblock %}
{% block description %}
<p>Paperwork for these events has been sent to treasury, but the full balance has not yet appeared on a ledger</p>
{% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'RIGS/invoice_list.html' %}
{% block title %}
Invoice Archive
{% endblock %}
{% block heading %}
All Invoices
{% endblock %}
{% block description %}
<p>This page displays all invoices: outstanding, paid, and void</p>
{% endblock %}

View File

@@ -126,7 +126,7 @@
<dd> <dd>
{% if user.api_key %} {% if user.api_key %}
<pre id="cal-url" data-url="http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}"></pre> <pre id="cal-url" data-url="http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}"></pre>
<small><a id="gcal-link" data-url="http://www.google.com/calendar/render?cid=http{{ request.is_secure|yesno:"s,"}}://{{ request.get_host }}{% url 'ics_calendar' api_pk=user.pk api_key=user.api_key %}" href="">Click here</a> to add to google calendar.<br/> <small><a id="gcal-link" data-url="https://support.google.com/calendar/answer/37100" href="">Click here</a> for instructions on adding to google calendar.<br/>
To sync from google calendar to mobile device, visit <a href="https://www.google.com/calendar/syncselect" target="_blank">this page</a> on your device and tick "RIGS Calendar".</small> To sync from google calendar to mobile device, visit <a href="https://www.google.com/calendar/syncselect" target="_blank">this page</a> on your device and tick "RIGS Calendar".</small>
{% else %} {% else %}
<pre>No API Key Generated</pre> <pre>No API Key Generated</pre>

View File

@@ -1,40 +1,21 @@
{% for change in version.field_changes %} {% for change in version.field_changes %}
<button title="Changes to {{ change.field.verbose_name }}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content=' <button title="Changes to {{ change.field.verbose_name }}" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %}
{% include "RIGS/version_changes_change.html" %}
{% if change.new %} {% endspaceless %}'>{{ change.field.verbose_name }}</button>
<div class="alert alert-success {% if change.long %}overflow-ellipsis{% endif %}">
{% if change.linebreaks %}
{{change.new|linebreaksbr}}
{% else %}
{{change.new}}
{% endif %}
</div>
{% endif %}
{% if change.old %}
<div class="alert alert-danger {% if change.long %}overflow-ellipsis{% endif %}">
{% if change.linebreaks %}
{{change.old|linebreaksbr}}
{% else %}
{{change.old}}
{% endif %}
</div>
{% endif %}
'>{{ change.field.verbose_name }}</button>
{% endfor %} {% endfor %}
{% for itemChange in version.item_changes %} {% for itemChange in version.item_changes %}
<button title="Changes to item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content=' <button title="Changes to item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'" type="button" class="btn btn-default btn-xs" data-container="body" data-html="true" data-trigger='hover' data-toggle="popover" data-content='{% spaceless %}
<ul class="list-group">
{% for change in itemChange.changes %} {% for change in itemChange.changes %}
<h4>{{ change.field.verbose_name }}</h4> <li class="list-group-item">
<h4 class="list-group-item-heading">{{ change.field.verbose_name }}</h4>
{% if change.new %}<div class="alert alert-success">{{change.new|linebreaksbr}}</div>{% endif %} {% include "RIGS/version_changes_change.html" %}
{% if change.old %}<div class="alert alert-danger">{{change.old|linebreaksbr}}</div>{% endif %} </li>
{% endfor %} {% endfor %}
</ul>
'>item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'</button> {% endspaceless %}'>item '{% if itemChange.new %}{{ itemChange.new.name }}{% else %}{{ itemChange.old.name }}{% endif %}'</button>
{% endfor %} {% endfor %}

View File

@@ -0,0 +1,34 @@
{# pass in variable "change" to this template #}
{% if change.linebreaks and change.new and change.old %}
{% for diff in change.diff %}
{% if diff.type == "insert" %}
<ins>{{ diff.text|linebreaksbr }}</ins>
{% elif diff.type == "delete" %}
<del>{{diff.text|linebreaksbr}}</del>
{% else %}
<span>{{diff.text|linebreaksbr}}</span>
{% endif %}
{% endfor %}
{% else %}
{% if change.old %}
<del {% if change.long %}class="overflow-ellipsis"{% endif %}>
{% if change.linebreaks %}
{{change.old|linebreaksbr}}
{% else %}
{{change.old}}
{% endif %}
</del>
{% endif %}
{% if change.new and change.old %}
<br/>
{% endif %}
{% if change.new %}
<ins {% if change.long %}class="overflow-ellipsis"{% endif %}>
{% if change.linebreaks %}
{{change.new|linebreaksbr}}
{% else %}
{{change.new}}
{% endif %}
</ins>
{% endif %}
{% endif %}

View File

@@ -35,6 +35,7 @@
<td>Version ID</td> <td>Version ID</td>
<td>User</td> <td>User</td>
<td>Changes</td> <td>Changes</td>
<td>Comment</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -46,11 +47,14 @@
<td>{{ version.revision.user.name }}</td> <td>{{ version.revision.user.name }}</td>
<td> <td>
{% if version.old == None %} {% if version.old == None %}
Object Created {{object|to_class_name}} Created
{% else %} {% else %}
{% include 'RIGS/version_changes.html' %} {% include 'RIGS/version_changes.html' %}
{% endif %} {% endif %}
</td> </td>
<td>
{{ version.revision.comment }}
</td>
</tr> </tr>
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@@ -4,7 +4,7 @@ from django.test.client import Client
from django.core import mail from django.core import mail
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import StaleElementReferenceException from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support.ui import WebDriverWait
from RIGS import models from RIGS import models
import re import re
@@ -159,6 +159,7 @@ class EventTest(LiveServerTestCase):
self.browser = webdriver.Firefox() self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3) # Set implicit wait session wide self.browser.implicitly_wait(3) # Set implicit wait session wide
self.browser.maximize_window()
os.environ['RECAPTCHA_TESTING'] = 'True' os.environ['RECAPTCHA_TESTING'] = 'True'
def tearDown(self): def tearDown(self):
@@ -195,233 +196,245 @@ class EventTest(LiveServerTestCase):
self.browser.get(self.live_server_url + '/rigboard/') self.browser.get(self.live_server_url + '/rigboard/')
def testRigCreate(self): def testRigCreate(self):
# Requests address
self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back
self.authenticate('/event/create/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
# Check has slided up correctly - second save button hidden
save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
self.assertFalse(save.is_displayed())
# Click Rig button
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
# Slider expands and save button visible
self.assertTrue(save.is_displayed())
form = self.browser.find_element_by_tag_name('form')
# Create new person
add_person_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "add")]')
add_person_button.click()
# See modal has opened
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
# Fill person form out and submit
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
# See new person selected
person1 = models.Person.objects.get(name="Test Person 1")
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Change mind and add another
add_person_button.click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person2 = models.Person.objects.get(name="Test Person 2")
self.assertEqual(person2.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Have to do this explcitly to force the wait for it to update
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person2.pk, int(option.get_attribute("value")))
# Was right the first time, change it back
person_select = form.find_element_by_xpath(
'//button[@data-id="id_person"]')
person_select.send_keys(person1.name)
person_dropped = form.find_element_by_xpath(
'//ul[contains(@class, "inner selectpicker")]//span[contains(text(), "%s")]' % person1.name)
person_dropped.click()
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Edit Person 1 to have a better name
form.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Edit Person", modal.find_element_by_tag_name('h3').text)
name = modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]')
self.assertEqual(person1.name, name.get_attribute('value'))
name.clear()
name.send_keys('Rig ' + person1.name)
name.send_keys(Keys.ENTER)
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person1 = models.Person.objects.get(pk=person1.pk)
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Create organisation
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Organisation.objects.get(name="Test Organisation")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_organisation"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_organisation"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Create veneue
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_venue" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Venue.objects.get(name="Test Venue")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_venue"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_venue"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Set start date/time
form.find_element_by_id('id_start_date').send_keys('3015-05-25')
form.find_element_by_id('id_start_time').send_keys('06:59')
# Set end date/time
form.find_element_by_id('id_end_date').send_keys('4000-06-27')
form.find_element_by_id('id_end_time').send_keys('07:00')
# Add item
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 1")
modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
e.send_keys(Keys.ENTER) # enter submit
# Confirm item has been saved to json field
objectitems = self.browser.execute_script("return objectitems;")
self.assertEqual(1, len(objectitems))
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
self.assertEqual("Test Item 1", testitem['name'])
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
# See new item appear in table
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
self.assertIn("This is an item description", row.find_element_by_xpath('//div[@class="item-description"]').text)
self.assertEqual(u'£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
self.assertEqual(u'£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
# Check totals
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
# Attempt to save - missing title
save.click()
# See error
error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]')
self.assertTrue(error.is_displayed())
# Should only have one error message
self.assertEqual("Name", error.find_element_by_xpath('//dt[1]').text)
self.assertEqual("This field is required.", error.find_element_by_xpath('//dd[1]/ul/li').text)
# don't need error so close it
error.find_element_by_xpath('//div[contains(@class, "alert-danger")]//button[@class="close"]').click()
try: try:
self.assertFalse(error.is_displayed()) # Requests address
except StaleElementReferenceException: self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back
self.authenticate('/event/create/')
wait = WebDriverWait(self.browser, 10) #setup WebDriverWait to use later (to wait for animations)
wait.until(animation_is_finished())
# Check has slided up correctly - second save button hidden
save = self.browser.find_element_by_xpath(
'(//button[@type="submit"])[3]')
self.assertFalse(save.is_displayed())
# Click Rig button
self.browser.find_element_by_xpath('//button[.="Rig"]').click()
# Slider expands and save button visible
self.assertTrue(save.is_displayed())
form = self.browser.find_element_by_tag_name('form')
# Create new person
wait.until(animation_is_finished())
add_person_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "add")]')
add_person_button.click()
# See modal has opened
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
# Fill person form out and submit
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 1")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
# See new person selected
person1 = models.Person.objects.get(name="Test Person 1")
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Change mind and add another
wait.until(animation_is_finished())
add_person_button.click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Person", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Person 2")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person2 = models.Person.objects.get(name="Test Person 2")
self.assertEqual(person2.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Have to do this explcitly to force the wait for it to update
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person2.pk, int(option.get_attribute("value")))
# Was right the first time, change it back
person_select = form.find_element_by_xpath(
'//button[@data-id="id_person"]')
person_select.send_keys(person1.name)
person_dropped = form.find_element_by_xpath(
'//ul[contains(@class, "inner selectpicker")]//span[contains(text(), "%s")]' % person1.name)
person_dropped.click()
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
option = form.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Edit Person 1 to have a better name
form.find_element_by_xpath(
'//a[@data-target="#id_person" and contains(@href, "%s/edit/")]' % person1.pk).click()
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Edit Person", modal.find_element_by_tag_name('h3').text)
name = modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]')
self.assertEqual(person1.name, name.get_attribute('value'))
name.clear()
name.send_keys('Rig ' + person1.name)
name.send_keys(Keys.ENTER)
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
person1 = models.Person.objects.get(pk=person1.pk)
self.assertEqual(person1.name, form.find_element_by_xpath(
'//button[@data-id="id_person"]/span').text)
# Create organisation
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_organisation" and contains(@href, "add")]')
add_button.click()
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Organisation", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Organisation")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Organisation.objects.get(name="Test Organisation")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_organisation"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_organisation"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Create venue
wait.until(animation_is_finished())
add_button = self.browser.find_element_by_xpath(
'//a[@data-target="#id_venue" and contains(@href, "add")]')
wait.until(animation_is_finished())
add_button.click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id('modal')
wait.until(animation_is_finished())
self.assertTrue(modal.is_displayed())
self.assertIn("Add Venue", modal.find_element_by_tag_name('h3').text)
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@id="id_name"]').send_keys("Test Venue")
modal.find_element_by_xpath(
'//div[@id="modal"]//input[@type="submit"]').click()
# See it is selected
wait.until(animation_is_finished())
self.assertFalse(modal.is_displayed())
obj = models.Venue.objects.get(name="Test Venue")
self.assertEqual(obj.name, form.find_element_by_xpath(
'//button[@data-id="id_venue"]/span').text)
# and backend
option = form.find_element_by_xpath(
'//select[@id="id_venue"]//option[@selected="selected"]')
self.assertEqual(obj.pk, int(option.get_attribute("value")))
# Set start date/time
form.find_element_by_id('id_start_date').send_keys('3015-05-25')
form.find_element_by_id('id_start_time').send_keys('06:59')
# Set end date/time
form.find_element_by_id('id_end_date').send_keys('4000-06-27')
form.find_element_by_id('id_end_time').send_keys('07:00')
# Add item
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
wait.until(animation_is_finished())
modal = self.browser.find_element_by_id("itemModal")
modal.find_element_by_id("item_name").send_keys("Test Item 1")
modal.find_element_by_id("item_description").send_keys("This is an item description\nthat for reasons unkown spans two lines")
e = modal.find_element_by_id("item_quantity")
e.click()
e.send_keys(Keys.UP)
e.send_keys(Keys.UP)
e = modal.find_element_by_id("item_cost")
e.send_keys("23.95")
e.send_keys(Keys.ENTER) # enter submit
# Confirm item has been saved to json field
objectitems = self.browser.execute_script("return objectitems;")
self.assertEqual(1, len(objectitems))
testitem = objectitems["-1"]['fields'] # as we are deliberately creating this we know the ID
self.assertEqual("Test Item 1", testitem['name'])
self.assertEqual("2", testitem['quantity']) # test a couple of "worse case" fields
# See new item appear in table
row = self.browser.find_element_by_id('item--1') # ID number is known, see above
self.assertIn("Test Item 1", row.find_element_by_xpath('//span[@class="name"]').text)
self.assertIn("This is an item description", row.find_element_by_xpath('//div[@class="item-description"]').text)
self.assertEqual(u'£ 23.95', row.find_element_by_xpath('//tr[@id="item--1"]/td[2]').text)
self.assertEqual("2", row.find_element_by_xpath('//td[@class="quantity"]').text)
self.assertEqual(u'£ 47.90', row.find_element_by_xpath('//tr[@id="item--1"]/td[4]').text)
# Check totals
self.assertEqual("47.90", self.browser.find_element_by_id('sumtotal').text)
self.assertIn("(TBC)", self.browser.find_element_by_id('vat-rate').text)
self.assertEqual("9.58", self.browser.find_element_by_id('vat').text)
self.assertEqual("57.48", self.browser.find_element_by_id('total').text)
# Attempt to save - missing title
save.click()
# See error
error = self.browser.find_element_by_xpath('//div[contains(@class, "alert-danger")]')
self.assertTrue(error.is_displayed())
# Should only have one error message
self.assertEqual("Name", error.find_element_by_xpath('//dt[1]').text)
self.assertEqual("This field is required.", error.find_element_by_xpath('//dd[1]/ul/li').text)
# don't need error so close it
error.find_element_by_xpath('//div[contains(@class, "alert-danger")]//button[@class="close"]').click()
try:
self.assertFalse(error.is_displayed())
except StaleElementReferenceException:
pass
except:
self.assertFail("Element does not appear to have been deleted")
# Check at least some data is preserved. Some = all will be there
option = self.browser.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Set title
e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name')
e.send_keys(Keys.ENTER)
# 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)
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
pass pass
except:
self.assertFail("Element does not appear to have been deleted")
# Check at least some data is preserved. Some = all will be there
option = self.browser.find_element_by_xpath(
'//select[@id="id_person"]//option[@selected="selected"]')
self.assertEqual(person1.pk, int(option.get_attribute("value")))
# Set title
e = self.browser.find_element_by_id('id_name')
e.send_keys('Test Event Name')
e.send_keys(Keys.ENTER)
# See redirected to success page
event = models.Event.objects.get(name='Test Event Name')
self.assertIn("N0000%d | Test Event Name"%event.pk, self.browser.find_element_by_xpath('//h1').text)
def testEventDuplicate(self): def testEventDuplicate(self):
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end") testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end")
@@ -608,9 +621,10 @@ class EventTest(LiveServerTestCase):
save.click() save.click()
# See redirected to success page # See redirected to success page
successTitle = self.browser.find_element_by_xpath('//h1').text
event = models.Event.objects.get(name='Test Event Name') event = models.Event.objects.get(name='Test Event Name')
self.assertIn("N0000%d | Test Event Name"%event.pk, self.browser.find_element_by_xpath('//h1').text) self.assertIn("N0000%d | Test Event Name"%event.pk, successTitle)
def testRigNonRig(self): def testRigNonRig(self):
self.browser.get(self.live_server_url + '/event/create/') self.browser.get(self.live_server_url + '/event/create/')
# Gets redirected to login and back # Gets redirected to login and back

View File

@@ -1,6 +1,8 @@
import pytz
from django.conf import settings
from django.test import TestCase from django.test import TestCase
from RIGS import models from RIGS import models
from datetime import date, timedelta from datetime import date, timedelta, datetime, time
from decimal import * from decimal import *
@@ -201,6 +203,72 @@ class EventTestCase(TestCase):
event.status = models.Event.PROVISIONAL event.status = models.Event.PROVISIONAL
event.save() event.save()
def test_earliest_time(self):
event = models.Event(name="TE ET", start_date=date(2016, 01, 01))
# Just a start date
self.assertEqual(event.earliest_time, date(2016, 01, 01))
# With start time
event.start_time = time(9, 00)
self.assertEqual(event.earliest_time, self.create_datetime(2016, 1, 1, 9, 00))
# With access time
event.access_at = self.create_datetime(2015, 12, 03, 9, 57)
self.assertEqual(event.earliest_time, event.access_at)
# With meet time
event.meet_at = self.create_datetime(2015, 12, 03, 9, 55)
self.assertEqual(event.earliest_time, event.meet_at)
# Check order isn't important
event.start_date = date(2015, 12, 03)
self.assertEqual(event.earliest_time, self.create_datetime(2015, 12, 03, 9, 00))
def test_latest_time(self):
event = models.Event(name="TE LT", start_date=date(2016, 01, 01))
# Just start date
self.assertEqual(event.latest_time, event.start_date)
# Just end date
event.end_date = date(2016, 1, 2)
self.assertEqual(event.latest_time, event.end_date)
# With end time
event.end_time = time(23, 00)
self.assertEqual(event.latest_time, self.create_datetime(2016, 1, 2, 23, 00))
def test_in_bounds(self):
manager = models.Event.objects
events = [
manager.create(name="TE IB0", start_date='2016-01-02'), # yes no
manager.create(name="TE IB1", start_date='2015-12-31', end_date='2016-01-04'),
# 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'),
# 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))
self.assertIn(events[0], in_bounds)
self.assertIn(events[1], in_bounds)
self.assertIn(events[2], in_bounds)
self.assertIn(events[3], in_bounds)
self.assertIn(events[4], in_bounds)
self.assertIn(events[5], in_bounds)
self.assertNotIn(events[6], in_bounds)
def create_datetime(self, year, month, day, hour, min):
tz = pytz.timezone(settings.TIME_ZONE)
return tz.localize(datetime(year, month, day, hour, min))
class EventItemTestCase(TestCase): class EventItemTestCase(TestCase):
def setUp(self): def setUp(self):

157
RIGS/test_unit.py Normal file
View File

@@ -0,0 +1,157 @@
from django.core.urlresolvers import reverse
from django.test import TestCase
from datetime import date
from RIGS import models
from django.core.exceptions import ObjectDoesNotExist
class TestAdminMergeObjects(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.persons = {
1: models.Person.objects.create(name="Person 1"),
2: models.Person.objects.create(name="Person 2"),
3: models.Person.objects.create(name="Person 3"),
}
cls.organisations = {
1: models.Organisation.objects.create(name="Organisation 1"),
2: models.Organisation.objects.create(name="Organisation 2"),
3: models.Organisation.objects.create(name="Organisation 3"),
}
cls.venues = {
1: models.Venue.objects.create(name="Venue 1"),
2: models.Venue.objects.create(name="Venue 2"),
3: models.Venue.objects.create(name="Venue 3"),
}
cls.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today(), person=cls.persons[1],
organisation=cls.organisations[3], venue=cls.venues[2]),
2: models.Event.objects.create(name="TE E2", start_date=date.today(), person=cls.persons[2],
organisation=cls.organisations[2], venue=cls.venues[3]),
3: models.Event.objects.create(name="TE E3", start_date=date.today(), person=cls.persons[3],
organisation=cls.organisations[1], venue=cls.venues[1]),
4: models.Event.objects.create(name="TE E4", start_date=date.today(), person=cls.persons[3],
organisation=cls.organisations[3], venue=cls.venues[3]),
}
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
def test_merge_confirmation(self):
change_url = reverse('admin:RIGS_venue_changelist')
data = {
'action': 'merge',
'_selected_action': [unicode(val.pk) for key, val in self.venues.iteritems()]
}
response = self.client.post(change_url, data, follow=True)
self.assertContains(response, "The following objects will be merged")
for key, venue in self.venues.iteritems():
self.assertContains(response, venue.name)
def test_merge_no_master(self):
change_url = reverse('admin:RIGS_venue_changelist')
data = {'action': 'merge',
'_selected_action': [unicode(val.pk) for key, val in self.venues.iteritems()],
'post': 'yes',
}
response = self.client.post(change_url, data, follow=True)
self.assertContains(response, "An error occured")
def test_venue_merge(self):
change_url = reverse('admin:RIGS_venue_changelist')
data = {'action': 'merge',
'_selected_action': [unicode(self.venues[1].pk), unicode(self.venues[2].pk)],
'post': 'yes',
'master': self.venues[1].pk
}
response = self.client.post(change_url, data, follow=True)
self.assertContains(response, "Objects successfully merged")
self.assertContains(response, self.venues[1].name)
# Check the master copy still exists
self.assertTrue(models.Venue.objects.get(pk=self.venues[1].pk))
# Check the un-needed venue has been disposed of
self.assertRaises(ObjectDoesNotExist, models.Venue.objects.get, pk=self.venues[2].pk)
# Check the one we didn't delete is still there
self.assertEqual(models.Venue.objects.get(pk=self.venues[3].pk), self.venues[3])
# Check the events have been moved to the master venue
for key, event in self.events.iteritems():
updatedEvent = models.Event.objects.get(pk=event.pk)
if event.venue == self.venues[3]: # The one we left in place
continue
self.assertEqual(updatedEvent.venue, self.venues[1])
def test_person_merge(self):
change_url = reverse('admin:RIGS_person_changelist')
data = {'action': 'merge',
'_selected_action': [unicode(self.persons[1].pk), unicode(self.persons[2].pk)],
'post': 'yes',
'master': self.persons[1].pk
}
response = self.client.post(change_url, data, follow=True)
self.assertContains(response, "Objects successfully merged")
self.assertContains(response, self.persons[1].name)
# Check the master copy still exists
self.assertTrue(models.Person.objects.get(pk=self.persons[1].pk))
# Check the un-needed people have been disposed of
self.assertRaises(ObjectDoesNotExist, models.Person.objects.get, pk=self.persons[2].pk)
# Check the one we didn't delete is still there
self.assertEqual(models.Person.objects.get(pk=self.persons[3].pk), self.persons[3])
# Check the events have been moved to the master person
for key, event in self.events.iteritems():
updatedEvent = models.Event.objects.get(pk=event.pk)
if event.person == self.persons[3]: # The one we left in place
continue
self.assertEqual(updatedEvent.person, self.persons[1])
def test_organisation_merge(self):
change_url = reverse('admin:RIGS_organisation_changelist')
data = {'action': 'merge',
'_selected_action': [unicode(self.organisations[1].pk), unicode(self.organisations[2].pk)],
'post': 'yes',
'master': self.organisations[1].pk
}
response = self.client.post(change_url, data, follow=True)
self.assertContains(response, "Objects successfully merged")
self.assertContains(response, self.organisations[1].name)
# Check the master copy still exists
self.assertTrue(models.Organisation.objects.get(pk=self.organisations[1].pk))
# Check the un-needed organisations have been disposed of
self.assertRaises(ObjectDoesNotExist, models.Organisation.objects.get, pk=self.organisations[2].pk)
# Check the one we didn't delete is still there
self.assertEqual(models.Organisation.objects.get(pk=self.organisations[3].pk), self.organisations[3])
# Check the events have been moved to the master organisation
for key, event in self.events.iteritems():
updatedEvent = models.Event.objects.get(pk=event.pk)
if event.organisation == self.organisations[3]: # The one we left in place
continue
self.assertEqual(updatedEvent.organisation, self.organisations[1])

View File

@@ -69,6 +69,8 @@ urlpatterns = patterns('',
# Rigboard # Rigboard
url(r'^rigboard/$', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'), url(r'^rigboard/$', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'), url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/calendar/(?P<view>(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')), url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')),
url(r'^rigboard/activity/$', url(r'^rigboard/activity/$',
permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()), permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()),

View File

@@ -1,26 +1,18 @@
import logging import logging
from django.views import generic
from django.core.urlresolvers import reverse_lazy
from django.shortcuts import get_object_or_404
from django.template import RequestContext
from django.template.loader import get_template
from django.conf import settings
from django.http import HttpResponse
from django.db.models import Q
from django.contrib import messages
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.views import generic
# Versioning # Versioning
import reversion import reversion
import simplejson
from reversion.models import Version from reversion.models import Version
from django.contrib.contenttypes.models import ContentType # Used to lookup the content_type from django.contrib.contenttypes.models import ContentType # Used to lookup the content_type
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.db.models import IntegerField, EmailField, TextField
from django.db.models import ForeignKey, IntegerField, EmailField, TextField from diff_match_patch import diff_match_patch
from RIGS import models, forms from RIGS import models
import datetime import datetime
import re
logger = logging.getLogger('tec.pyrigs') logger = logging.getLogger('tec.pyrigs')
@@ -28,11 +20,10 @@ logger = logging.getLogger('tec.pyrigs')
def model_compare(oldObj, newObj, excluded_keys=[]): def model_compare(oldObj, newObj, excluded_keys=[]):
# recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects # recieves two objects of the same model, and compares them. Returns an array of FieldCompare objects
try: try:
theFields = oldObj._meta.fields #This becomes deprecated in Django 1.8!!!!!!!!!!!!! (but an alternative becomes available) theFields = oldObj._meta.fields # This becomes deprecated in Django 1.8!!!!!!!!!!!!! (but an alternative becomes available)
except AttributeError: except AttributeError:
theFields = newObj._meta.fields theFields = newObj._meta.fields
class FieldCompare(object): class FieldCompare(object):
def __init__(self, field=None, old=None, new=None): def __init__(self, field=None, old=None, new=None):
self.field = field self.field = field
@@ -50,13 +41,13 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
@property @property
def new(self): def new(self):
return self.display_value(self._new) return self.display_value(self._new)
@property @property
def long(self): def long(self):
if isinstance(self.field, EmailField): if isinstance(self.field, EmailField):
return True return True
return False return False
@property @property
def linebreaks(self): def linebreaks(self):
@@ -64,27 +55,54 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
return True return True
return False return False
@property
def diff(self):
oldText = unicode(self.display_value(self._old)) or ""
newText = unicode(self.display_value(self._new)) or ""
dmp = diff_match_patch()
diffs = dmp.diff_main(oldText, newText)
dmp.diff_cleanupSemantic(diffs)
outputDiffs = []
for (op, data) in diffs:
if op == dmp.DIFF_INSERT:
outputDiffs.append({'type': 'insert', 'text': data})
elif op == dmp.DIFF_DELETE:
outputDiffs.append({'type': 'delete', 'text': data})
elif op == dmp.DIFF_EQUAL:
outputDiffs.append({'type': 'equal', 'text': data})
return outputDiffs
changes = [] changes = []
for thisField in theFields: for thisField in theFields:
name = thisField.name name = thisField.name
if name in excluded_keys:
continue # if we're excluding this field, skip over it
oldValue = getattr(oldObj, name, None) if name in excluded_keys:
newValue = getattr(newObj, name, None) continue # if we're excluding this field, skip over it
try:
oldValue = getattr(oldObj, name, None)
except ObjectDoesNotExist:
oldValue = None
try:
newValue = getattr(newObj, name, None)
except ObjectDoesNotExist:
newValue = None
try: try:
bothBlank = (not oldValue) and (not newValue) bothBlank = (not oldValue) and (not newValue)
if oldValue != newValue and not bothBlank: if oldValue != newValue and not bothBlank:
compare = FieldCompare(thisField,oldValue,newValue) compare = FieldCompare(thisField, oldValue, newValue)
changes.append(compare) changes.append(compare)
except TypeError: # logs issues with naive vs tz-aware datetimes except TypeError: # logs issues with naive vs tz-aware datetimes
logger.error('TypeError when comparing models') logger.error('TypeError when comparing models')
return changes return changes
def compare_event_items(old, new): def compare_event_items(old, new):
# Recieves two event version objects and compares their items, returns an array of ItemCompare objects # Recieves two event version objects and compares their items, returns an array of ItemCompare objects
@@ -99,39 +117,43 @@ def compare_event_items(old, new):
self.changes = changes self.changes = changes
# Build some dicts of what we have # Build some dicts of what we have
item_dict = {} # build a list of items, key is the item_pk item_dict = {} # build a list of items, key is the item_pk
for version in old_item_versions: # put all the old versions in a list for version in old_item_versions: # put all the old versions in a list
compare = ItemCompare(old=version.object_version.object) if version.field_dict["event"] == old.object_id_int:
item_dict[version.object_id] = compare compare = ItemCompare(old=version.object_version.object)
item_dict[version.object_id] = compare
for version in new_item_versions: # go through the new versions for version in new_item_versions: # go through the new versions
try: if version.field_dict["event"] == new.object_id_int:
compare = item_dict[version.object_id] # see if there's a matching old version try:
compare.new = version.object_version.object # then add the new version to the dictionary compare = item_dict[version.object_id] # see if there's a matching old version
except KeyError: # there's no matching old version, so add this item to the dictionary by itself compare.new = version.object_version.object # then add the new version to the dictionary
compare = ItemCompare(new=version.object_version.object) except KeyError: # there's no matching old version, so add this item to the dictionary by itself
compare = ItemCompare(new=version.object_version.object)
item_dict[version.object_id] = compare # update the dictionary with the changes
changes = [] item_dict[version.object_id] = compare # update the dictionary with the changes
changes = []
for (_, compare) in item_dict.items(): for (_, compare) in item_dict.items():
compare.changes = model_compare(compare.old, compare.new, ['id','event','order']) # see what's changed compare.changes = model_compare(compare.old, compare.new, ['id', 'event', 'order']) # see what's changed
if len(compare.changes) >= 1: if len(compare.changes) >= 1:
changes.append(compare) # transfer into a sequential array to make it easier to deal with later changes.append(compare) # transfer into a sequential array to make it easier to deal with later
return changes return changes
def get_versions_for_model(models): def get_versions_for_model(models):
content_types = [] content_types = []
for model in models: for model in models:
content_types.append(ContentType.objects.get_for_model(model)) content_types.append(ContentType.objects.get_for_model(model))
versions = reversion.models.Version.objects.filter( versions = reversion.models.Version.objects.filter(
content_type__in = content_types, content_type__in=content_types,
).select_related("revision").order_by("-pk") ).select_related("revision").order_by("-pk")
return versions return versions
def get_previous_version(version): def get_previous_version(version):
thisId = version.object_id thisId = version.object_id
thisVersionId = version.pk thisVersionId = version.pk
@@ -139,17 +161,19 @@ def get_previous_version(version):
versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId) versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId)
try: try:
previousVersions = versions.filter(revision_id__lt=version.revision_id).latest(field_name='revision__date_created') previousVersions = versions.filter(revision_id__lt=version.revision_id).latest(
field_name='revision__date_created')
except ObjectDoesNotExist: except ObjectDoesNotExist:
return False return False
return previousVersions return previousVersions
def get_changes_for_version(newVersion, oldVersion=None):
#Pass in a previous version if you already know it (for efficiancy)
#if not provided then it will be looked up in the database
if oldVersion == None: def get_changes_for_version(newVersion, oldVersion=None):
# Pass in a previous version if you already know it (for efficiancy)
# if not provided then it will be looked up in the database
if oldVersion == None:
oldVersion = get_previous_version(newVersion) oldVersion = get_previous_version(newVersion)
modelClass = newVersion.content_type.model_class() modelClass = newVersion.content_type.model_class()
@@ -173,6 +197,7 @@ def get_changes_for_version(newVersion, oldVersion=None):
return compare return compare
class VersionHistory(generic.ListView): class VersionHistory(generic.ListView):
model = reversion.revisions.Version model = reversion.revisions.Version
template_name = "RIGS/version_history.html" template_name = "RIGS/version_history.html"
@@ -188,7 +213,7 @@ class VersionHistory(generic.ListView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
thisModel = self.kwargs['model'] thisModel = self.kwargs['model']
context = super(VersionHistory, self).get_context_data(**kwargs) context = super(VersionHistory, self).get_context_data(**kwargs)
versions = context['object_list'] versions = context['object_list']
@@ -197,81 +222,82 @@ class VersionHistory(generic.ListView):
items = [] items = []
for versionNo, thisVersion in enumerate(versions): for versionNo, thisVersion in enumerate(versions):
if versionNo >= len(versions)-1: if versionNo >= len(versions) - 1:
thisItem = get_changes_for_version(thisVersion, None) thisItem = get_changes_for_version(thisVersion, None)
else: else:
thisItem = get_changes_for_version(thisVersion, versions[versionNo+1]) thisItem = get_changes_for_version(thisVersion, versions[versionNo + 1])
items.append(thisItem) items.append(thisItem)
context['object_list'] = items context['object_list'] = items
context['object'] = thisObject context['object'] = thisObject
return context return context
class ActivityTable(generic.ListView): class ActivityTable(generic.ListView):
model = reversion.revisions.Version model = reversion.revisions.Version
template_name = "RIGS/activity_table.html" template_name = "RIGS/activity_table.html"
paginate_by = 25 paginate_by = 25
def get_queryset(self): def get_queryset(self):
versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation]) versions = get_versions_for_model([models.Event, models.Venue, models.Person, models.Organisation])
return versions return versions
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
# Call the base implementation first to get a context # Call the base implementation first to get a context
context = super(ActivityTable, self).get_context_data(**kwargs) context = super(ActivityTable, self).get_context_data(**kwargs)
items = [] items = []
for thisVersion in context['object_list']: for thisVersion in context['object_list']:
thisItem = get_changes_for_version(thisVersion, None) thisItem = get_changes_for_version(thisVersion, None)
items.append(thisItem) items.append(thisItem)
context ['object_list'] = items context['object_list'] = items
return context return context
class ActivityFeed(generic.ListView): class ActivityFeed(generic.ListView):
model = reversion.revisions.Version model = reversion.revisions.Version
template_name = "RIGS/activity_feed_data.html" template_name = "RIGS/activity_feed_data.html"
paginate_by = 25 paginate_by = 25
def get_queryset(self): def get_queryset(self):
versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation]) versions = get_versions_for_model([models.Event, models.Venue, models.Person, models.Organisation])
return versions return versions
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
maxTimeDelta = [] maxTimeDelta = []
maxTimeDelta.append({ 'maxAge':datetime.timedelta(days=1), 'group':datetime.timedelta(hours=1)}) maxTimeDelta.append({'maxAge': datetime.timedelta(days=1), 'group': datetime.timedelta(hours=1)})
maxTimeDelta.append({ 'maxAge':None, 'group':datetime.timedelta(days=1)}) maxTimeDelta.append({'maxAge': None, 'group': datetime.timedelta(days=1)})
# Call the base implementation first to get a context # Call the base implementation first to get a context
context = super(ActivityFeed, self).get_context_data(**kwargs) context = super(ActivityFeed, self).get_context_data(**kwargs)
items = [] items = []
for thisVersion in context['object_list']: for thisVersion in context['object_list']:
thisItem = get_changes_for_version(thisVersion, None) thisItem = get_changes_for_version(thisVersion, None)
if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None: if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None:
thisItem['withPrevious'] = False thisItem['withPrevious'] = False
if len(items)>=1: if len(items) >= 1:
timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem['revision'].date_created timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem[
'revision'].date_created
timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created
timeTogether = False timeTogether = False
for params in maxTimeDelta: for params in maxTimeDelta:
if params['maxAge'] is None or timeAgo <= params['maxAge']: if params['maxAge'] is None or timeAgo <= params['maxAge']:
timeTogether = timeDiff < params['group'] timeTogether = timeDiff < params['group']
break break
sameUser = thisItem['revision'].user == items[-1]['revision'].user sameUser = thisItem['revision'].user == items[-1]['revision'].user
thisItem['withPrevious'] = timeTogether & sameUser thisItem['withPrevious'] = timeTogether & sameUser
items.append(thisItem) items.append(thisItem)
context ['object_list'] = items context['object_list'] = items
return context return context

Binary file not shown.

View File

@@ -1,3 +1,4 @@
diff-match-patch==20121119
dj-database-url==0.3.0 dj-database-url==0.3.0
dj-static==0.0.6 dj-static==0.0.6
Django==1.8.2 Django==1.8.2
@@ -19,7 +20,7 @@ python-dateutil==2.4.2
pytz==2015.4 pytz==2015.4
raven==5.8.1 raven==5.8.1
reportlab==3.1.44 reportlab==3.1.44
selenium==2.46.0 selenium==2.53.1
simplejson==3.7.2 simplejson==3.7.2
six==1.9.0 six==1.9.0
sqlparse==0.1.15 sqlparse==0.1.15

0
subhire/__init__.py Normal file
View File

6
subhire/admin.py Normal file
View File

@@ -0,0 +1,6 @@
from django.contrib import admin
import models
# Register your models here.
admin.site.register(models.Hire)
admin.site.register(models.Provider)

View File

@@ -0,0 +1,43 @@
# -*- 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='Hire',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)),
('description', models.TextField(null=True, blank=True)),
('start_date', models.DateField()),
('end_date', models.DateField()),
('start_transport', models.IntegerField(blank=True, null=True, choices=[(0, b'TEC Transport'), (1, b'Provider Transports')])),
('mic', models.ForeignKey(related_name='hire_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
],
),
migrations.CreateModel(
name='Provider',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=50)),
('phone', models.CharField(max_length=15, null=True, blank=True)),
('email', models.EmailField(max_length=254, null=True, blank=True)),
('address', models.TextField(null=True, blank=True)),
('notes', models.TextField(null=True, blank=True)),
],
),
migrations.AddField(
model_name='hire',
name='provider',
field=models.ForeignKey(blank=True, to='subhire.Provider', null=True),
),
]

View File

39
subhire/models.py Normal file
View File

@@ -0,0 +1,39 @@
import reversion
from django.db import models
from django.conf import settings
from django.utils.encoding import python_2_unicode_compatible
# Create your models here.
class Hire(models.Model):
WE_TRANSPORT = 0
THEY_TRANSPORT = 1
TRANSPORT_CHOICES = (
(WE_TRANSPORT, 'TEC Transport'),
(THEY_TRANSPORT, 'Provider Transports'),
)
name = models.CharField(max_length=255)
description = models.TextField(blank=True, null=True)
provider = models.ForeignKey('Provider', blank=True, null=True)
start_date = models.DateField()
end_date = models.DateField()
start_transport = models.IntegerField(
choices=TRANSPORT_CHOICES, blank=True, null=True)
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='hire_mic', blank=True, null=True,
verbose_name="MIC")
class Provider(models.Model):
name = models.CharField(max_length=50)
phone = models.CharField(max_length=15, blank=True, null=True)
email = models.EmailField(blank=True, null=True)
address = models.TextField(blank=True, null=True)
notes = models.TextField(blank=True, null=True)
@property
def latest_hires(self):
return self.hire_set.order_by('-start_date').select_related()

3
subhire/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
subhire/views.py Normal file
View File

@@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@@ -14,14 +14,16 @@
<link rel="icon" type="image/png" href="{% static "imgs/pyrigs-avatar.png" %}"> <link rel="icon" type="image/png" href="{% static "imgs/pyrigs-avatar.png" %}">
<link rel="apple-touch-icon" href="{% static "imgs/pyrigs-avatar.png" %}"> <link rel="apple-touch-icon" href="{% static "imgs/pyrigs-avatar.png" %}">
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet'
type='text/css'>
<link rel="stylesheet" type="text/css" href="{% static "css/screen.css" %}"> <link rel="stylesheet" type="text/css" href="{% static "css/screen.css" %}">
{% block css %} {% block css %}
{% endblock %} {% endblock %}
<script src="//code.jquery.com/jquery-latest.min.js"></script> <script src="https://code.jquery.com/jquery-1.8.3.min.js"
integrity="sha256-YcbK69I5IXQftf/mYD8WY0/KmEDCv1asggHpJk1trM8=" crossorigin="anonymous"></script>
<script src="https://cdn.ravenjs.com/1.3.0/jquery,native/raven.min.js"></script> <script src="https://cdn.ravenjs.com/1.3.0/jquery,native/raven.min.js"></script>
<script>Raven.config('{% sentry_public_dsn %}').install()</script> <script>Raven.config('{% sentry_public_dsn %}').install()</script>
{% block preload_js %} {% block preload_js %}
@@ -46,32 +48,40 @@
<div class="navbar-collapse"> <div class="navbar-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="/">Home</a></li> <li><a href="/">Home</a></li>
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Rigboard<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Rigboard<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span> Rigboard</a></li> <li><a href="{% url 'rigboard' %}"><span class="glyphicon glyphicon-list"></span>
<li><a href="{% url 'event_archive' %}"><span class="glyphicon glyphicon-book"></span> Archive</a></li> Rigboard</a></li>
<li><a href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span> Calendar</a></li> <li><a href="{% url 'event_archive' %}"><span class="glyphicon glyphicon-book"></span>
{% if perms.RIGS.view_event %} Archive</a></li>
<li><a href="{% url 'activity_table' %}"><span class="glyphicon glyphicon-random"></span> Recent Changes</a></li> <li><a href="{% url 'web_calendar' %}"><span class="glyphicon glyphicon-calendar"></span>
{% endif %} Calendar</a></li>
{% if perms.RIGS.add_event %} {% if perms.RIGS.view_event %}
<li><a href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span> New Event</a></li> <li><a href="{% url 'activity_table' %}"><span
{% endif %} class="glyphicon glyphicon-random"></span> Recent Changes</a></li>
{% endif %}
</ul> {% if perms.RIGS.add_event %}
</li> <li><a href="{% url 'event_create' %}"><span class="glyphicon glyphicon-plus"></span>
New Event</a></li>
{% endif %}
</ul>
</li>
{% endif %} {% endif %}
{% if perms.RIGS.view_invoice %} {% if perms.RIGS.view_invoice %}
<li class="dropdown"> <li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a> <a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Active</a></li>
{% if perms.RIGS.add_invoice %} {% if perms.RIGS.add_invoice %}
<li><a href="{% url 'invoice_waiting' %}"><span class="glyphicon glyphicon-briefcase"></span> Waiting</a></li> <li><a href="{% url 'invoice_waiting' %}"><span
class="glyphicon glyphicon-briefcase"></span> Waiting</a></li>
{% endif %} {% endif %}
<li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span> Archive</a></li> <li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Outstanding</a>
</li>
<li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span>
Archive</a></li>
</ul> </ul>
</li> </li>
{% endif %} {% endif %}
@@ -84,7 +94,7 @@
{% if perms.RIGS.view_venue %} {% if perms.RIGS.view_venue %}
<li><a href="{% url 'venue_list' %}">Venues</a></li> <li><a href="{% url 'venue_list' %}">Venues</a></li>
{% endif %} {% endif %}
</ul> </ul>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li class="dropdown"> <li class="dropdown">
@@ -144,10 +154,6 @@
{% endif %} {% endif %}
{% endblock %} {% endblock %}
<div class="col-sm-12 text-center">
Reminder: Please consider carefully before booking rigs at this moment
</div>
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
@@ -181,7 +187,7 @@
jQuery(document).on('click', '.modal-href', function (e) { jQuery(document).on('click', '.modal-href', function (e) {
$link = jQuery(this); $link = jQuery(this);
// Anti modal inception // Anti modal inception
if($link.parents('#modal').length == 0) { if ($link.parents('#modal').length == 0) {
e.preventDefault(); e.preventDefault();
modaltarget = $link.data('target'); modaltarget = $link.data('target');
modalobject = ""; modalobject = "";
@@ -193,11 +199,11 @@
var easter_egg = new Konami(); var easter_egg = new Konami();
easter_egg.code = function() { easter_egg.code = function () {
var s = document.createElement('script'); var s = document.createElement('script');
s.type='text/javascript'; s.type = 'text/javascript';
document.body.appendChild(s); document.body.appendChild(s);
s.src='{% static "js/asteroids.min.js"%}'; s.src = '{% static "js/asteroids.min.js"%}';
ga('send', 'event', 'easter_egg', 'activated'); ga('send', 'event', 'easter_egg', 'activated');
} }
easter_egg.load(); easter_egg.load();

View File

@@ -6,7 +6,7 @@
<form action="{% url 'login' %}" method="post" role="form">{% csrf_token %} <form action="{% url 'login' %}" method="post" role="form">{% csrf_token %}
<div class="form-group"> <div class="form-group">
<label for="id_username">{{ form.username.label }}</label> <label for="id_username">{{ form.username.label }}</label>
{% render_field form.username class+="form-control" placeholder=form.username.label %} {% render_field form.username class+="form-control" placeholder=form.username.label autofocus="" %}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="{{ form.password.id_for_label }}">{{ form.password.label }}</label> <label for="{{ form.password.id_for_label }}">{{ form.password.label }}</label>