Merge branch 'master' into markdown

This commit is contained in:
Tom Price
2016-07-13 23:39:42 +01:00
43 changed files with 2631 additions and 391 deletions

32
.codeclimate.yml Normal file
View File

@@ -0,0 +1,32 @@
---
engines:
csslint:
enabled: true
duplication:
enabled: true
config:
languages:
- ruby
- javascript
- python
- php
eslint:
enabled: true
fixme:
enabled: true
radon:
enabled: true
rubocop:
enabled: true
ratings:
paths:
- "**.css"
- "**.inc"
- "**.js"
- "**.jsx"
- "**.module"
- "**.php"
- "**.py"
- "**.rb"
exclude_paths:
- config/

6
.coveragerc Normal file
View File

@@ -0,0 +1,6 @@
[run]
source =
./
omit =
*/migrations/*

2
.csslintrc Normal file
View File

@@ -0,0 +1,2 @@
--exclude-exts=.min.css
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes

1
.eslintignore Normal file
View File

@@ -0,0 +1 @@
**/*{.,-}min.js

213
.eslintrc Normal file
View File

@@ -0,0 +1,213 @@
ecmaFeatures:
modules: true
jsx: true
env:
amd: true
browser: true
es6: true
jquery: true
node: true
# http://eslint.org/docs/rules/
rules:
# Possible Errors
comma-dangle: [2, never]
no-cond-assign: 2
no-console: 0
no-constant-condition: 2
no-control-regex: 2
no-debugger: 2
no-dupe-args: 2
no-dupe-keys: 2
no-duplicate-case: 2
no-empty: 2
no-empty-character-class: 2
no-ex-assign: 2
no-extra-boolean-cast: 2
no-extra-parens: 0
no-extra-semi: 2
no-func-assign: 2
no-inner-declarations: [2, functions]
no-invalid-regexp: 2
no-irregular-whitespace: 2
no-negated-in-lhs: 2
no-obj-calls: 2
no-regex-spaces: 2
no-sparse-arrays: 2
no-unexpected-multiline: 2
no-unreachable: 2
use-isnan: 2
valid-jsdoc: 0
valid-typeof: 2
# Best Practices
accessor-pairs: 2
block-scoped-var: 0
complexity: [2, 6]
consistent-return: 0
curly: 0
default-case: 0
dot-location: 0
dot-notation: 0
eqeqeq: 2
guard-for-in: 2
no-alert: 2
no-caller: 2
no-case-declarations: 2
no-div-regex: 2
no-else-return: 0
no-empty-label: 2
no-empty-pattern: 2
no-eq-null: 2
no-eval: 2
no-extend-native: 2
no-extra-bind: 2
no-fallthrough: 2
no-floating-decimal: 0
no-implicit-coercion: 0
no-implied-eval: 2
no-invalid-this: 0
no-iterator: 2
no-labels: 0
no-lone-blocks: 2
no-loop-func: 2
no-magic-number: 0
no-multi-spaces: 0
no-multi-str: 0
no-native-reassign: 2
no-new-func: 2
no-new-wrappers: 2
no-new: 2
no-octal-escape: 2
no-octal: 2
no-proto: 2
no-redeclare: 2
no-return-assign: 2
no-script-url: 2
no-self-compare: 2
no-sequences: 0
no-throw-literal: 0
no-unused-expressions: 2
no-useless-call: 2
no-useless-concat: 2
no-void: 2
no-warning-comments: 0
no-with: 2
radix: 2
vars-on-top: 0
wrap-iife: 2
yoda: 0
# Strict
strict: 0
# Variables
init-declarations: 0
no-catch-shadow: 2
no-delete-var: 2
no-label-var: 2
no-shadow-restricted-names: 2
no-shadow: 0
no-undef-init: 2
no-undef: 0
no-undefined: 0
no-unused-vars: 0
no-use-before-define: 0
# Node.js and CommonJS
callback-return: 2
global-require: 2
handle-callback-err: 2
no-mixed-requires: 0
no-new-require: 0
no-path-concat: 2
no-process-exit: 2
no-restricted-modules: 0
no-sync: 0
# Stylistic Issues
array-bracket-spacing: 0
block-spacing: 0
brace-style: 0
camelcase: 0
comma-spacing: 0
comma-style: 0
computed-property-spacing: 0
consistent-this: 0
eol-last: 0
func-names: 0
func-style: 0
id-length: 0
id-match: 0
indent: 0
jsx-quotes: 0
key-spacing: 0
linebreak-style: 0
lines-around-comment: 0
max-depth: 0
max-len: 0
max-nested-callbacks: 0
max-params: 0
max-statements: [2, 30]
new-cap: 0
new-parens: 0
newline-after-var: 0
no-array-constructor: 0
no-bitwise: 0
no-continue: 0
no-inline-comments: 0
no-lonely-if: 0
no-mixed-spaces-and-tabs: 0
no-multiple-empty-lines: 0
no-negated-condition: 0
no-nested-ternary: 0
no-new-object: 0
no-plusplus: 0
no-restricted-syntax: 0
no-spaced-func: 0
no-ternary: 0
no-trailing-spaces: 0
no-underscore-dangle: 0
no-unneeded-ternary: 0
object-curly-spacing: 0
one-var: 0
operator-assignment: 0
operator-linebreak: 0
padded-blocks: 0
quote-props: 0
quotes: 0
require-jsdoc: 0
semi-spacing: 0
semi: 0
sort-vars: 0
space-after-keywords: 0
space-before-blocks: 0
space-before-function-paren: 0
space-before-keywords: 0
space-in-parens: 0
space-infix-ops: 0
space-return-throw-case: 0
space-unary-ops: 0
spaced-comment: 0
wrap-regex: 0
# ECMAScript 6
arrow-body-style: 0
arrow-parens: 0
arrow-spacing: 0
constructor-super: 0
generator-star-spacing: 0
no-arrow-condition: 0
no-class-assign: 0
no-const-assign: 0
no-dupe-class-members: 0
no-this-before-super: 0
no-var: 0
object-shorthand: 0
prefer-arrow-callback: 0
prefer-const: 0
prefer-reflect: 0
prefer-spread: 0
prefer-template: 0
require-yield: 0

6
.gitignore vendored
View File

@@ -1,3 +1,6 @@
tmp/
db.sqlite3
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
@@ -25,9 +28,6 @@ var/
# Continer extras # Continer extras
.vagrant .vagrant
_builds
_steps
_projects
# PyInstaller # PyInstaller
# Usually these files are written by a python script from a template # Usually these files are written by a python script from a template

1156
.rubocop.yml Normal file

File diff suppressed because it is too large Load Diff

21
.travis.yml Normal file
View File

@@ -0,0 +1,21 @@
language: python
python:
"2.7"
before_install:
- "export DISPLAY=:99.0"
- "/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -ac -screen 0 1280x1024x16"
install:
- pip install -r requirements.txt
- pip install coveralls codeclimate-test-reporter
before_script:
- python manage.py collectstatic --noinput
script:
- coverage run manage.py test RIGS
after_success:
- coveralls
- codeclimate-test-reporter

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/
@@ -23,10 +21,19 @@ SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else '
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
TEMPLATE_DEBUG = True 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']
if STAGING:
ALLOWED_HOSTS.append('.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 = (
@@ -55,6 +62,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,6 @@
# TEC PA & Lighting - PyRIGS # # TEC PA & Lighting - PyRIGS #
[![wercker status](https://app.wercker.com/status/2dbe0517c3d83859c985ffc5a55a2802/m/master "wercker status")](https://app.wercker.com/project/bykey/2dbe0517c3d83859c985ffc5a55a2802) [![Build Status](https://travis-ci.org/nottinghamtec/PyRIGS.svg?branch=develop)](https://travis-ci.org/nottinghamtec/PyRIGS)
[![Coverage Status](https://coveralls.io/repos/github/nottinghamtec/PyRIGS/badge.svg?branch=develop)](https://coveralls.io/github/nottinghamtec/PyRIGS?branch=develop)
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.
@@ -74,5 +75,22 @@ 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.
### Sample Data ###
Sample data is available to aid local development and user acceptance testing. To load this data into your local database, first ensure the database is empty:
```
python manage.py flush
```
Then load the sample data using the command:
```
python manage.py generateSampleData
```
4 user accounts are created for convenience:
|Username |Password |
|---------|---------|
|superuser|superuser|
|finance |finance |
|keyholder|keyholder|
|basic |basic |
### 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')
@@ -83,25 +94,64 @@ class InvoiceVoid(generic.View):
return HttpResponseRedirect(reverse_lazy('invoice_list')) return HttpResponseRedirect(reverse_lazy('invoice_list'))
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
class InvoiceDelete(generic.DeleteView):
model = models.Invoice
def get(self, request, pk):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).get(pk)
def post(self, request, pk):
obj = self.get_object()
if obj.payment_set.all().count() > 0:
messages.info(self.request, 'To delete an invoice, delete the payments first.')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': obj.pk}))
return super(InvoiceDelete, self).post(pk)
def get_success_url(self):
return self.request.POST.get('next')
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
class InvoiceWaiting(generic.ListView): class InvoiceWaiting(generic.ListView):
model = models.Event model = models.Event
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
@@ -113,13 +163,14 @@ class InvoiceEvent(generic.View):
if created: if created:
invoice.invoice_date = datetime.date.today() invoice.invoice_date = datetime.date.today()
messages.success(self.request, 'Invoice created successfully')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk})) return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))
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()

View File

View File

View File

@@ -0,0 +1,248 @@
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth.models import Group, Permission
from django.db import transaction
import reversion
import datetime
import random
from RIGS import models
class Command(BaseCommand):
help = 'Adds sample data to use for testing'
can_import_settings = True
people = []
organisations = []
venues = []
profiles = []
keyholder_group = None
finance_group = None
def handle(self, *args, **options):
from django.conf import settings
if not (settings.DEBUG or settings.STAGING):
raise CommandError('You cannot run this command in production')
random.seed('Some object to seed the random number generator') # otherwise it is done by time, which could lead to inconsistant tests
with transaction.atomic():
models.VatRate.objects.create(start_at='2014-03-05',rate=0.20,comment='test1')
self.setupGenericProfiles()
self.setupPeople()
self.setupOrganisations()
self.setupVenues()
self.setupGroups()
self.setupEvents()
self.setupUsefulProfiles()
def setupPeople(self):
names = ["Regulus Black","Sirius Black","Lavender Brown","Cho Chang","Vincent Crabbe","Vincent Crabbe","Bartemius Crouch","Fleur Delacour","Cedric Diggory","Alberforth Dumbledore","Albus Dumbledore","Dudley Dursley","Petunia Dursley","Vernon Dursley","Argus Filch","Seamus Finnigan","Nicolas Flamel","Cornelius Fudge","Goyle","Gregory Goyle","Hermione Granger","Rubeus Hagrid","Igor Karkaroff","Viktor Krum","Bellatrix Lestrange","Alice Longbottom","Frank Longbottom","Neville Longbottom","Luna Lovegood","Xenophilius Lovegood","Remus Lupin","Draco Malfoy","Lucius Malfoy","Narcissa Malfoy","Olympe Maxime","Minerva McGonagall","Mad-Eye Moody","Peter Pettigrew","Harry Potter","James Potter","Lily Potter","Quirinus Quirrell","Tom Riddle","Mary Riddle","Lord Voldemort","Rita Skeeter","Severus Snape","Nymphadora Tonks","Dolores Janes Umbridge","Arthur Weasley","Bill Weasley","Charlie Weasley","Fred Weasley","George Weasley","Ginny Weasley","Molly Weasley","Percy Weasley","Ron Weasley","Dobby","Fluffy","Hedwig","Moaning Myrtle","Aragog","Grawp"]
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newPerson = models.Person.objects.create(name=name)
if i % 3 == 0:
newPerson.email = "address@person.com"
if i % 5 == 0:
newPerson.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newPerson.address = "1 Person Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newPerson.phone = "01234 567894"
newPerson.save()
self.people.append(newPerson)
def setupOrganisations(self):
names = ["Acme, inc.","Widget Corp","123 Warehousing","Demo Company","Smith and Co.","Foo Bars","ABC Telecom","Fake Brothers","QWERTY Logistics","Demo, inc.","Sample Company","Sample, inc","Acme Corp","Allied Biscuit","Ankh-Sto Associates","Extensive Enterprise","Galaxy Corp","Globo-Chem","Mr. Sparkle","Globex Corporation","LexCorp","LuthorCorp","North Central Positronics","Omni Consimer Products","Praxis Corporation","Sombra Corporation","Sto Plains Holdings","Tessier-Ashpool","Wayne Enterprises","Wentworth Industries","ZiffCorp","Bluth Company","Strickland Propane","Thatherton Fuels","Three Waters","Water and Power","Western Gas & Electric","Mammoth Pictures","Mooby Corp","Gringotts","Thrift Bank","Flowers By Irene","The Legitimate Businessmens Club","Osato Chemicals","Transworld Consortium","Universal Export","United Fried Chicken","Virtucon","Kumatsu Motors","Keedsler Motors","Powell Motors","Industrial Automation","Sirius Cybernetics Corporation","U.S. Robotics and Mechanical Men","Colonial Movers","Corellian Engineering Corporation","Incom Corporation","General Products","Leeding Engines Ltd.","Blammo","Input, Inc.","Mainway Toys","Videlectrix","Zevo Toys","Ajax","Axis Chemical Co.","Barrytron","Carrys Candles","Cogswell Cogs","Spacely Sprockets","General Forge and Foundry","Duff Brewing Company","Dunder Mifflin","General Services Corporation","Monarch Playing Card Co.","Krustyco","Initech","Roboto Industries","Primatech","Sonky Rubber Goods","St. Anky Beer","Stay Puft Corporation","Vandelay Industries","Wernham Hogg","Gadgetron","Burleigh and Stronginthearm","BLAND Corporation","Nordyne Defense Dynamics","Petrox Oil Company","Roxxon","McMahon and Tate","Sixty Second Avenue","Charles Townsend Agency","Spade and Archer","Megadodo Publications","Rouster and Sideways","C.H. Lavatory and Sons","Globo Gym American Corp","The New Firm","SpringShield","Compuglobalhypermeganet","Data Systems","Gizmonic Institute","Initrode","Taggart Transcontinental","Atlantic Northern","Niagular","Plow King","Big Kahuna Burger","Big T Burgers and Fries","Chez Quis","Chotchkies","The Frying Dutchman","Klimpys","The Krusty Krab","Monks Diner","Milliways","Minuteman Cafe","Taco Grande","Tip Top Cafe","Moes Tavern","Central Perk","Chasers"]
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newOrganisation = models.Organisation.objects.create(name=name)
if i % 2 == 0:
newOrganisation.has_su_account = True
if i % 3 == 0:
newOrganisation.email = "address@organisation.com"
if i % 5 == 0:
newOrganisation.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newOrganisation.address = "1 Organisation Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newOrganisation.phone = "01234 567894"
newOrganisation.save()
self.organisations.append(newOrganisation)
def setupVenues(self):
names = ["Bear Island","Crossroads Inn","Deepwood Motte","The Dreadfort","The Eyrie","Greywater Watch","The Iron Islands","Karhold","Moat Cailin","Oldstones","Raventree Hall","Riverlands","The Ruby Ford","Saltpans","Seagard","Torrhen's Square","The Trident","The Twins","The Vale of Arryn","The Whispering Wood","White Harbor","Winterfell","The Arbor","Ashemark","Brightwater Keep","Casterly Rock","Clegane's Keep","Dragonstone","Dorne","God's Eye","The Golden Tooth","Harrenhal","Highgarden","Horn Hill","Fingers","King's Landing","Lannisport","Oldtown","Rainswood","Storm's End","Summerhall","Sunspear","Tarth","Castle Black","Craster's Keep","Fist of the First Men","The Frostfangs","The Gift","The Skirling Pass","The Wall","Asshai","Astapor","Braavos","The Dothraki Sea","Lys","Meereen","Myr","Norvos","Pentos","Qarth","Qohor","The Red Waste","Tyrosh","Vaes Dothrak","Valyria","Village of the Lhazareen","Volantis","Yunkai"]
for i, name in enumerate(names):
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
newVenue = models.Venue.objects.create(name=name)
if i % 2 == 0:
newVenue.three_phase_available = True
if i % 3 == 0:
newVenue.email = "address@venue.com"
if i % 5 == 0:
newVenue.notes = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
if i % 7 == 0:
newVenue.address = "1 Venue Test Street \n Demoton \n United States of TEC \n RMRF 567"
if i % 9 == 0:
newVenue.phone = "01234 567894"
newVenue.save()
self.venues.append(newVenue)
def setupGroups(self):
self.keyholder_group = Group.objects.create(name='Keyholders')
self.finance_group = Group.objects.create(name='Finance')
keyholderPerms = ["add_event","change_event","view_event","add_eventitem","change_eventitem","delete_eventitem","add_organisation","change_organisation","view_organisation","add_person","change_person","view_person","view_profile","add_venue","change_venue","view_venue"]
financePerms = ["change_event","view_event","add_eventitem","change_eventitem","add_invoice","change_invoice","view_invoice","add_organisation","change_organisation","view_organisation","add_payment","change_payment","delete_payment","add_person","change_person","view_person"]
for permId in keyholderPerms:
self.keyholder_group.permissions.add(Permission.objects.get(codename=permId))
for permId in financePerms:
self.finance_group.permissions.add(Permission.objects.get(codename=permId))
def setupGenericProfiles(self):
names = ["Clara Oswin Oswald","Rory Williams","Amy Pond","River Song","Martha Jones","Donna Noble","Jack Harkness","Mickey Smith","Rose Tyler"]
for i, name in enumerate(names):
newProfile = models.Profile.objects.create(username=name.replace(" ",""), first_name=name.split(" ")[0], last_name=name.split(" ")[-1],
email=name.replace(" ","")+"@example.com",
initials="".join([ j[0].upper() for j in name.split() ]))
if i % 2 == 0:
newProfile.phone = "01234 567894"
newProfile.save()
self.profiles.append(newProfile)
def setupUsefulProfiles(self):
superUser = models.Profile.objects.create(username="superuser", first_name="Super", last_name="User", initials="SU",
email="superuser@example.com", is_superuser=True, is_active=True, is_staff=True)
superUser.set_password('superuser')
superUser.save()
financeUser = models.Profile.objects.create(username="finance", first_name="Finance", last_name="User", initials="FU",
email="financeuser@example.com", is_active=True)
financeUser.groups.add(self.finance_group)
financeUser.groups.add(self.keyholder_group)
financeUser.set_password('finance')
financeUser.save()
keyholderUser = models.Profile.objects.create(username="keyholder", first_name="Keyholder", last_name="User", initials="KU",
email="keyholderuser@example.com", is_active=True)
keyholderUser.groups.add(self.keyholder_group)
keyholderUser.set_password('keyholder')
keyholderUser.save()
basicUser = models.Profile.objects.create(username="basic", first_name="Basic", last_name="User", initials="BU",
email="basicuser@example.com", is_active=True)
basicUser.set_password('basic')
basicUser.save()
def setupEvents(self):
names = ["Outdoor Concert","Hall Open Mic Night","Festival","Weekend Event","Magic Show","Society Ball","Evening Show","Talent Show","Acoustic Evening","Hire of Things","SU Event","End of Term Show","Theatre Show","Outdoor Fun Day","Summer Carnival","Open Days","Magic Show","Awards Ceremony","Debating Event","Club Night","DJ Evening","Building Projection","Choir Concert"]
descriptions = ["A brief desciption of the event","This event is boring","Probably wont happen","Warning: this has lots of kit"]
notes = ["The client came into the office at some point","Who knows if this will happen", "Probably should check this event", "Maybe not happening", "Run away!"]
itemOptions = [{'name': 'Speakers', 'description': 'Some really really big speakers \n these are very loud', 'quantity': 2, 'cost': 200.00},
{'name': 'Projector', 'description': 'Some kind of video thinamejig, probably with unnecessary processing for free', 'quantity': 1, 'cost': 500.00},
{'name': 'Lighting Desk', 'description': 'Cannot provide guarentee that it will work', 'quantity': 1, 'cost': 200.52},
{'name': 'Moving lights', 'description': 'Flashy lights, with the copper', 'quantity': 8, 'cost': 50.00},
{'name': 'Microphones', 'description': 'Make loud noise \n you will want speakers with this', 'quantity': 5, 'cost': 0.50},
{'name': 'Sound Mixer Thing', 'description': 'Might be analogue, might be digital', 'quantity': 1, 'cost': 100.00},
{'name': 'Electricity', 'description': 'You need this', 'quantity': 1, 'cost': 200.00},
{'name': 'Crew', 'description': 'Costs nothing, because reasons', 'quantity': 1, 'cost': 0.00},
{'name': 'Loyalty Discount', 'description': 'Have some negative moneys', 'quantity': 1, 'cost': -50.00}]
dayDelta = -120 # start adding events from 4 months ago
for i in range(150): # Let's add 100 events
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
name = names[i%len(names)]
startDate = datetime.date.today() + datetime.timedelta(days=dayDelta)
dayDelta = dayDelta + random.randint(0,3)
newEvent = models.Event.objects.create(name=name, start_date=startDate)
if random.randint(0,2) > 1: # 1 in 3 have a start time
newEvent.start_time = datetime.time(random.randint(15,20))
if random.randint(0,2) > 1: # of those, 1 in 3 have an end time on the same day
newEvent.end_time = datetime.time(random.randint(21,23))
elif random.randint(0,1)>0: # half of the others finish early the next day
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=1)
newEvent.end_time = datetime.time(random.randint(0,5))
elif random.randint(0,2)>1: # 1 in 3 of the others finish a few days ahead
newEvent.end_date = newEvent.start_date + datetime.timedelta(days=random.randint(1,4))
if random.randint(0,6) > 0: # 5 in 6 have MIC
newEvent.mic = random.choice(self.profiles)
if random.randint(0,6) > 0: # 5 in 6 have organisation
newEvent.organisation = random.choice(self.organisations)
if random.randint(0,6) > 0: # 5 in 6 have person
newEvent.person = random.choice(self.people)
if random.randint(0,6) > 0: # 5 in 6 have venue
newEvent.venue = random.choice(self.venues)
# Could have any status, equally weighted
newEvent.status = random.choice([models.Event.BOOKED,models.Event.CONFIRMED,models.Event.PROVISIONAL, models.Event.CANCELLED])
newEvent.dry_hire = (random.randint(0,7)==0) # 1 in 7 are dry hire
if random.randint(0,1) > 0: # 1 in 2 have description
newEvent.description = random.choice(descriptions)
if random.randint(0,1) > 0: # 1 in 2 have notes
newEvent.notes = random.choice(notes)
newEvent.save()
# Now add some items
for j in range(random.randint(1,5)):
itemData = itemOptions[random.randint(0,len(itemOptions)-1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
while newEvent.sum_total < 0:
itemData = itemOptions[random.randint(0,len(itemOptions)-1)]
newItem = models.EventItem.objects.create(event=newEvent, order=j, **itemData)
newItem.save()
with reversion.create_revision():
reversion.set_user(random.choice(self.profiles))
if newEvent.start_date < datetime.date.today(): # think about adding an invoice
if random.randint(0,2) > 0: # 2 in 3 have had paperwork sent to treasury
newInvoice = models.Invoice.objects.create(event=newEvent)
if newEvent.status is models.Event.CANCELLED: # void cancelled events
newInvoice.void = True
elif random.randint(0,2)>1: # 1 in 3 have been paid
models.Payment.objects.create(invoice=newInvoice, amount=newInvoice.balance, date=datetime.date.today())

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
@@ -152,7 +155,7 @@ class Organisation(models.Model, RevisionMixin):
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', on_delete=models.SET_NULL, 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,20 +409,20 @@ 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
@@ -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):
@@ -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
@@ -521,6 +526,10 @@ class Invoice(models.Model):
def balance(self): def balance(self):
return self.sum_total - self.payment_total return self.sum_total - self.payment_total
@property
def is_closed(self):
return self.balance == 0 or self.void
def __str__(self): def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance) return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)

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

@@ -26,9 +26,17 @@
class="hidden-xs">Duplicate</span></a> class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %} {% if event.is_rig %}
{% if perms.RIGS.add_invoice %} {% if perms.RIGS.add_invoice %}
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span <a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
class="glyphicon glyphicon-gbp"></span> <span {% if event.invoice and event.invoice.is_closed %}
class="hidden-xs">Invoice</span></a> btn-success
{% elif event.invoice %}
btn-warning
{% else %}
btn-danger
{% endif %}
" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span>
<span class="hidden-xs">Invoice</span></a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@@ -152,7 +160,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 }} {% if object.based_on.mic %}by {{ object.based_on.mic.name }}{% endif %} {{ object.based_on.name }} {% if object.based_on.mic %}by {{ object.based_on.mic.name }}{% endif %}
</a> </a>
{% endif %} {% endif %}
</dd> </dd>
@@ -191,9 +199,17 @@
class="hidden-xs">Duplicate</span></a> class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %} {% if event.is_rig %}
{% if perms.RIGS.add_invoice %} {% if perms.RIGS.add_invoice %}
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span <a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
class="glyphicon glyphicon-gbp"></span> <span {% if event.invoice and event.invoice.is_closed %}
class="hidden-xs">Invoice</span></a> btn-success
{% elif event.invoice %}
btn-warning
{% else %}
btn-danger
{% endif %}
" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span>
<span class="hidden-xs">Invoice</span></a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@@ -228,9 +244,17 @@
class="hidden-xs">Duplicate</span></a> class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %} {% if event.is_rig %}
{% if perms.RIGS.add_invoice %} {% if perms.RIGS.add_invoice %}
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span <a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
class="glyphicon glyphicon-gbp"></span> <span {% if event.invoice and event.invoice.is_closed %}
class="hidden-xs">Invoice</span></a> btn-success
{% elif event.invoice %}
btn-warning
{% else %}
btn-danger
{% endif %}
" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span>
<span class="hidden-xs">Invoice</span></a>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -28,9 +28,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);
@@ -62,11 +62,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() {
@@ -105,7 +106,7 @@
}); });
} }
}) });
$(document).ready(function () { $(document).ready(function () {
setupItemTable($("#{{ form.items_json.id_for_label }}").val()); setupItemTable($("#{{ form.items_json.id_for_label }}").val());
@@ -149,10 +150,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>Event #</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

@@ -3,7 +3,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>
@@ -25,7 +25,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

@@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block title %}Delete payment on invoice {{ object.invoice.pk }}{% endblock %}
{% block content %}
<div class="col-sm-offset-2 col-sm-8">
<div class="alert alert-danger" role="alert">
<h2>Delete invoice {{ object.pk }}</h2>
<p>Are you sure you wish to delete invoice {{ object.pk }}?</p>
<p class="text-center"><strong>This action cannot be undone!</strong></p>
<div class="row">
<div class="col-sm-12">
<form action="{{ action_link }}" method="post">{% csrf_token %}
<input type="hidden" name="next" value="{% url 'invoice_list' %}"/>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<input type="submit" value="Yes" class="btn btn-danger col-sm-1"/>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
<a href="{% url 'invoice_detail' object.pk %}" class="btn btn-default col-sm-1">No</a>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -11,6 +11,10 @@
<div class="col-sm-4 text-right"> <div class="col-sm-4 text-right">
<div class="btn-group btn-page"> <div class="btn-group btn-page">
<a href="{% url 'invoice_delete' object.pk %}" class="btn btn-default" title="Void Invoice">
<span class="glyphicon glyphicon-remove"></span> <span
class="hidden-xs">Delete</span>
</a>
<a href="{% url 'invoice_void' object.pk %}" class="btn btn-default" title="Void Invoice"> <a href="{% url 'invoice_void' object.pk %}" class="btn btn-default" title="Void Invoice">
<span class="glyphicon glyphicon-ban-circle"></span> <span <span class="glyphicon glyphicon-ban-circle"></span> <span
class="hidden-xs">Void</span> class="hidden-xs">Void</span>
@@ -38,8 +42,11 @@
</div> </div>
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="panel panel-{% if object.void %}danger{% else %}info{% endif %}"> <div class="panel panel-{% if object.is_closed %}success{% else %}warning{% endif %}">
<div class="panel-heading">Event Details</div> <div class="panel-heading">Event Details<span class="pull-right">
{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(OUTSTANDING){% endif %}
</span>
</div>
<div class="panel-body"> <div class="panel-body">
<dl class="dl-horizontal"> <dl class="dl-horizontal">
<dt>Event Number</dt> <dt>Event Number</dt>
@@ -109,6 +116,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,71 @@
{% 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>Invoice #</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>
<td class="{% if object.is_closed %}success{% else %}warning{% endif %}">{{ object.pk }}<br>
<span class="text-muted">{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(O/S){% endif %}</span></td>
<td class="
{% if object.event.cancelled %}
active text-muted
{% elif not object.event.is_rig %}
info
{% elif object.event.confirmed and object.event.mic %}
{# interpreated as (booked and mic) #}
success
{% elif object.event.mic %}
warning
{% else %}
danger
{% endif %}
"><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 }}{% if not object.event.mic %}, No MIC{% endif %}
</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

@@ -45,7 +45,7 @@
<td>{{ object.pk }}</td> <td>{{ object.pk }}</td>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
<td>{{ object.email }}</td> <td>{{ object.email }}</td>
<td>{{ object.phone }}</td> <td><a href="tel:{{ object.phone }}">{{ object.phone }}</a></td>
<td>{{ object.notes|yesno|capfirst }}</td> <td>{{ object.notes|yesno|capfirst }}</td>
<td>{{ object.union_account|yesno|capfirst }}</td> <td>{{ object.union_account|yesno|capfirst }}</td>
<td> <td>

View File

@@ -44,7 +44,7 @@
<td>{{ person.pk }}</td> <td>{{ person.pk }}</td>
<td>{{ person.name }}</td> <td>{{ person.name }}</td>
<td>{{ person.email }}</td> <td>{{ person.email }}</td>
<td>{{ person.phone }}</td> <td><a href="tel:{{ person.phone }}">{{ person.phone }}</a></td>
<td>{{ person.notes|yesno|capfirst }}</td> <td>{{ person.notes|yesno|capfirst }}</td>
<td> <td>
<a href="{% url 'person_detail' person.pk %}" class="btn btn-default modal-href"> <a href="{% url 'person_detail' person.pk %}" class="btn btn-default modal-href">

View File

@@ -71,7 +71,7 @@
<dd>{{object.initials}}</dd> <dd>{{object.initials}}</dd>
<dt>Phone</dt> <dt>Phone</dt>
<dd>{{object.phone}}</dd> <dd><a href="tel:{{ object.phone }}">{{object.phone}}</a></dd>
</dl> </dl>
{% if not request.is_ajax %} {% if not request.is_ajax %}
{% if object.pk == user.pk %} {% if object.pk == user.pk %}
@@ -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

@@ -45,7 +45,7 @@
<td>{{ object.pk }}</td> <td>{{ object.pk }}</td>
<td>{{ object.name }}</td> <td>{{ object.name }}</td>
<td>{{ object.email }}</td> <td>{{ object.email }}</td>
<td>{{ object.phone }}</td> <td><a href="tel:{{ object.phone }}">{{ object.phone }}</a></td>
<td>{{ object.notes|yesno|capfirst }}</td> <td>{{ object.notes|yesno|capfirst }}</td>
<td> <td>
<a href="{% url 'venue_detail' object.pk %}" class="btn btn-default modal-href"> <a href="{% url 'venue_detail' object.pk %}" class="btn btn-default modal-href">

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

@@ -1,18 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import re
from datetime import date, timedelta
import reversion
from django.core import mail
from django.db import transaction
from django.test import LiveServerTestCase from django.test import LiveServerTestCase
from django.test.client import Client from django.test.client import Client
from django.core import mail
from selenium import webdriver from selenium import webdriver
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
from selenium.webdriver.common.keys import Keys
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 os
from datetime import date, timedelta
from django.db import transaction
import reversion
import json
class UserRegistrationTest(LiveServerTestCase): class UserRegistrationTest(LiveServerTestCase):
@@ -103,7 +104,7 @@ class UserRegistrationTest(LiveServerTestCase):
# Check Email # Check Email
self.assertEqual(len(mail.outbox), 1) self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0] email = mail.outbox[0]
self.assertIn('activation required', email.subject) self.assertIn('John Smith "JS" activation required', email.subject)
urls = re.findall( urls = re.findall(
'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body) 'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
self.assertEqual(len(urls), 1) self.assertEqual(len(urls), 1)

View File

@@ -1,11 +1,240 @@
from datetime import date
from django.core.exceptions import ObjectDoesNotExist
from django.core.management import call_command
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from RIGS.templatetags.markdown_tags import markdown_filter from RIGS.templatetags.markdown_tags import markdown_filter
from RIGS import models from RIGS import models
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])
class TestInvoiceDelete(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.events = {
1: models.Event.objects.create(name="TE E1", start_date=date.today()),
2: models.Event.objects.create(name="TE E2", start_date=date.today())
}
cls.invoices = {
1: models.Invoice.objects.create(event=cls.events[1]),
2: models.Invoice.objects.create(event=cls.events[2])
}
cls.payments = {
1: models.Payment.objects.create(invoice=cls.invoices[1], date=date.today(), amount=12.34,
method=models.Payment.CASH)
}
def setUp(self):
self.profile.set_password('testuser')
self.profile.save()
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
def test_invoice_delete_allowed(self):
request_url = reverse('invoice_delete', kwargs={'pk': self.invoices[2].pk})
response = self.client.get(request_url, follow=True)
self.assertContains(response, "Are you sure")
# Check the invoice still exists
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[2].pk))
# Actually delete it
response = self.client.post(request_url, follow=True)
# Check the invoice is deleted
self.assertRaises(ObjectDoesNotExist, models.Invoice.objects.get, pk=self.invoices[2].pk)
def test_invoice_delete_not_allowed(self):
request_url = reverse('invoice_delete', kwargs={'pk': self.invoices[1].pk})
response = self.client.get(request_url, follow=True)
self.assertContains(response, "To delete an invoice, delete the payments first.")
# Check the invoice still exists
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
# Try to actually delete it
response = self.client.post(request_url, follow=True)
# Check this didn't work
self.assertTrue(models.Invoice.objects.get(pk=self.invoices[1].pk))
class TestSampleDataGenerator(TestCase):
@override_settings(DEBUG=True)
def test_generate_sample_data(self):
# Run the management command and check there are no exceptions
call_command('generateSampleData')
# Check there are lots of events
self.assertTrue(models.Event.objects.all().count() > 100)
def test_production_exception(self):
from django.core.management.base import CommandError
self.assertRaisesRegexp(CommandError, ".*production", call_command, 'generateSampleData')
class TestMarkdownTemplateTags(TestCase): class TestMarkdownTemplateTags(TestCase):
markdown = """ markdown = """
An h1 header An h1 header

View File

@@ -127,6 +127,9 @@ urlpatterns = patterns('',
url(r'^invoice/(?P<pk>\d+)/void/$', url(r'^invoice/(?P<pk>\d+)/void/$',
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()), permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()),
name='invoice_void'), name='invoice_void'),
url(r'^invoice/(?P<pk>\d+)/delete/$',
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceDelete.as_view()),
name='invoice_delete'),
url(r'^payment/create/$', url(r'^payment/create/$',
permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()), permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
name='payment_create'), name='payment_create'),

View File

@@ -1,27 +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 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')
@@ -29,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
@@ -76,12 +66,12 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
outputDiffs = [] outputDiffs = []
for (op, data) in diffs: for (op, data) in diffs:
if op == dmp.DIFF_INSERT: if op == dmp.DIFF_INSERT:
outputDiffs.append({'type':'insert', 'text':data}) outputDiffs.append({'type': 'insert', 'text': data})
elif op == dmp.DIFF_DELETE: elif op == dmp.DIFF_DELETE:
outputDiffs.append({'type':'delete', 'text':data}) outputDiffs.append({'type': 'delete', 'text': data})
elif op == dmp.DIFF_EQUAL: elif op == dmp.DIFF_EQUAL:
outputDiffs.append({'type':'equal', 'text':data}) outputDiffs.append({'type': 'equal', 'text': data})
return outputDiffs return outputDiffs
changes = [] changes = []
@@ -90,21 +80,29 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
name = thisField.name name = thisField.name
if name in excluded_keys: if name in excluded_keys:
continue # if we're excluding this field, skip over it continue # if we're excluding this field, skip over it
oldValue = getattr(oldObj, name, None) try:
newValue = getattr(newObj, name, None) 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
@@ -119,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 item_dict[version.object_id] = compare # update the dictionary with the changes
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
@@ -159,15 +161,17 @@ 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): def get_changes_for_version(newVersion, oldVersion=None):
#Pass in a previous version if you already know it (for efficiancy) # 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 not provided then it will be looked up in the database
if oldVersion == None: if oldVersion == None:
oldVersion = get_previous_version(newVersion) oldVersion = get_previous_version(newVersion)
@@ -193,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"
@@ -217,10 +222,10 @@ 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)
@@ -229,17 +234,17 @@ class VersionHistory(generic.ListView):
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)
@@ -249,24 +254,25 @@ class ActivityTable(generic.ListView):
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)
@@ -277,8 +283,9 @@ class ActivityFeed(generic.ListView):
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:
@@ -291,7 +298,6 @@ class ActivityFeed(generic.ListView):
items.append(thisItem) items.append(thisItem)
context ['object_list'] = items context['object_list'] = items
return context return context

53
app.json Normal file
View File

@@ -0,0 +1,53 @@
{
"name": "PyRIGS",
"description": "",
"scripts": {
"postdeploy": "python manage.py migrate && python manage.py generateSampleData"
},
"env": {
"DEBUG": {
"required": true
},
"STAGING": "1",
"EMAIL_FROM": {
"required": true
},
"EMAIL_HOST": {
"required": true
},
"EMAIL_HOST_PASSWORD": {
"required": true
},
"EMAIL_HOST_USER": {
"required": true
},
"EMAIL_PORT": {
"required": true
},
"EMAIL_USE_SSL": {
"required": true
},
"RECAPTCHA_PRIVATE_KEY": {
"required": true
},
"RECAPTCHA_PUBLIC_KEY": {
"required": true
},
"SECRET_KEY": {
"generator": "secret"
}
},
"formation": {
"web": {
"quantity": 1
}
},
"addons": [
"heroku-postgresql"
],
"buildpacks": [
{
"url": "heroku/python"
}
]
}

View File

@@ -22,7 +22,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.53.1 selenium==2.53.6
simplejson==3.7.2 simplejson==3.7.2
six==1.9.0 six==1.9.0
sqlparse==0.1.15 sqlparse==0.1.15

View File

@@ -14,7 +14,7 @@
<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' <link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet'
type='text/css'> type='text/css'>
@@ -74,12 +74,12 @@
<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 <li><a href="{% url 'invoice_waiting' %}"><span
class="glyphicon glyphicon-briefcase"></span> Waiting</a></li> class="glyphicon glyphicon-briefcase text-danger"></span> Waiting</a></li>
{% endif %} {% endif %}
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp text-warning"></span> Outstanding</a>
</li>
<li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span> <li><a href="{% url 'invoice_archive' %}"><span class="glyphicon glyphicon-book"></span>
Archive</a></li> Archive</a></li>
</ul> </ul>

View File

@@ -1 +1 @@
{{ user }} activation required {{ user|safe }} activation required

View File

@@ -1,103 +0,0 @@
# This references the default Python container from
# the Docker Hub with the 2.7 tag:
# https://registry.hub.docker.com/_/python/
# If you want to use a slim Python container with
# version 3.4.3 you would use: python:3.4-slim
# If you want Google's container you would reference google/python
# Read more about containers on our dev center
# http://devcenter.wercker.com/docs/containers/index.html
box: heroku/python
# You can also use services such as databases. Read more on our dev center:
# http://devcenter.wercker.com/docs/services/index.html
services:
# - id: postgres
# env:
# POSTGRES_PASSWORD: pyrigstesting
# POSTGRES_USER: pyrigs
# http://devcenter.wercker.com/docs/services/postgresql.html
# - mongodb
# http://devcenter.wercker.com/docs/services/mongodb.html
# This is the build pipeline. Pipelines are the core of wercker
# Read more about pipelines on our dev center
# http://devcenter.wercker.com/docs/pipelines/index.html
build:
# The steps that will be executed on build
# Steps make up the actions in your pipeline
# Read more about steps on our dev center:
# http://devcenter.wercker.com/docs/steps/index.html
steps:
- install-packages:
packages: firefox xvfb
- script:
name: Enable virtual display
code: |-
# Start xvfb which gives the context an virtual display
# which is required for tests that require an GUI
export DISPLAY=:99.0
start-stop-daemon --start --quiet --pidfile /tmp/xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1024x768x24 -ac +extension GLX +render -noreset
# Give xvfb time to start. 3 seconds is the default for all xvfb-run commands.
# sleep 3
- script:
name: virtualenv install
code: |
pip install virtualenv
# A step that sets up the python virtual environment
- virtualenv:
name: setup virtual environment
install_wheel: false # Enable wheel to speed up builds (experimental)
# # Use this virtualenv step for python 3.2
# - virtualenv
# name: setup virtual environment
# python_location: /usr/bin/python3.2
# # This pip-install clears the local wheel cache
# - pip-install:
# clean_wheel_dir: true
# A custom script step, name value is used in the UI
# and the code value contains the command that get executed
- script:
name: Python Version
code: |
echo "python version $(python --version) running"
echo "pip version $(pip --version) running"
# Django uses this to connect to the database
# - script:
# name: set environment
# code: |
# export DEBUG=0
# export DATABASE_URL="postgres://pyrigs:pyrigstesting@$POSTGRES_PORT_5432_TCP_ADDR:$POSTGRES_PORT_5432_TCP_PORT$POSTGRES_NAME"
# A step that executes `pip install` command.
- pip-install
# Install coverage
- script:
name: install coverage
code: |
pip install coverage
# Collect static files so the manifest storage knows where to look
- script:
name: collect static
code: |
python manage.py collectstatic --noinput
# Run python tests
- script:
name: run tests
code: |
coverage run manage.py test RIGS
- script:
name: collect coverage data
code: |
coverage report -m --include=PyRIGS/*.*,RIGS/*.* --omit=*/migrations/* | tee coverage.txt
coverage html --include=PyRIGS/*.*,RIGS/*.* --omit=*/migrations/*