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
__pycache__/
*.py[cod]
@@ -25,9 +28,6 @@ var/
# Continer extras
.vagrant
_builds
_steps
_projects
# PyInstaller
# 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:
user_object = models.Profile.objects.get(pk=userid)
except Profile.DoesNotExist:
except models.Profile.DoesNotExist:
return error_resp
if user_object.api_key != key:

View File

@@ -12,8 +12,6 @@ https://docs.djangoproject.com/en/1.7/ref/settings/
import os
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
# 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!
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
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']
ADMINS = (
@@ -55,6 +62,7 @@ INSTALLED_APPS = (
MIDDLEWARE_CLASSES = (
'raven.contrib.django.raven_compat.middleware.SentryResponseErrorIdMiddleware',
'django.middleware.security.SecurityMiddleware',
'reversion.middleware.RevisionMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',

View File

@@ -1,5 +1,6 @@
# 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.
@@ -74,5 +75,22 @@ python manage.py runserver
```
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 ###
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,16 +4,23 @@ from django.contrib.auth.admin import UserAdmin
from django.utils.translation import ugettext_lazy as _
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.
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.Venue, reversion.VersionAdmin)
admin.site.register(models.Event, reversion.VersionAdmin)
admin.site.register(models.EventItem, reversion.VersionAdmin)
admin.site.register(models.Invoice)
admin.site.register(models.Payment)
@admin.register(models.Profile)
class ProfileAdmin(UserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password')}),
@@ -33,4 +40,76 @@ class ProfileAdmin(UserAdmin):
form = forms.ProfileChangeForm
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 datetime
import re
from django.contrib import messages
from django.core.urlresolvers import reverse_lazy
from django.db import connection
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.shortcuts import get_object_or_404
from django.contrib import messages
import datetime
from django.template import RequestContext
from django.template.loader import get_template
from django.views import generic
from django.db.models import Q
from z3c.rml import rml2pdf
from RIGS import models
import re
class InvoiceIndex(generic.ListView):
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):
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
sql = "SELECT * FROM " \
"(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(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\") " \
"AS sub " \
"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):
model = models.Invoice
class InvoicePrint(generic.View):
def get(self, request, pk):
invoice = get_object_or_404(models.Invoice, pk=pk)
@@ -72,6 +82,7 @@ class InvoicePrint(generic.View):
response.write(pdfData)
return response
class InvoiceVoid(generic.View):
def get(self, *args, **kwargs):
pk = kwargs.get('pk')
@@ -83,25 +94,64 @@ class InvoiceVoid(generic.View):
return HttpResponseRedirect(reverse_lazy('invoice_list'))
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):
model = models.Invoice
template_name = 'RIGS/invoice_list_archive.html'
paginate_by = 25
class InvoiceWaiting(generic.ListView):
model = models.Event
paginate_by = 25
# paginate_by = 25
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):
return self.get_objects()
def get_objects(self):
# @todo find a way to select items
events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(),
invoice__isnull=True) \
.order_by('start_date') \
events = self.model.objects.filter(
(
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',
'organisation',
'venue', 'mic')
'venue', 'mic') \
.prefetch_related('items')
return events
@@ -113,6 +163,7 @@ class InvoiceEvent(generic.View):
if created:
invoice.invoice_date = datetime.date.today()
messages.success(self.request, 'Invoice created successfully')
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': invoice.pk}))

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,20 +1,21 @@
import datetime
import hashlib
import datetime, 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 pytz
import random
import string
from collections import Counter
from django.core.urlresolvers import reverse_lazy
from django.core.exceptions import ValidationError
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.
@python_2_unicode_compatible
class Profile(AbstractUser):
@@ -55,6 +56,7 @@ class Profile(AbstractUser):
('view_profile', 'Can view Profile'),
)
class RevisionMixin(object):
@property
def last_edited_at(self):
@@ -83,6 +85,7 @@ class RevisionMixin(object):
else:
return None
@reversion.register
@python_2_unicode_compatible
class Person(models.Model, RevisionMixin):
@@ -238,12 +241,18 @@ class Venue(models.Model, RevisionMixin):
class EventManager(models.Manager):
def current_events(self):
events = self.filter(
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
(models.Q(start_date__gte=datetime.date.today(), end_date__isnull=True, dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Starts after with no end
(models.Q(end_date__gte=datetime.date.today(), dry_hire=False) & ~models.Q(
status=Event.CANCELLED)) | # Ends after
(models.Q(dry_hire=True, start_date__gte=datetime.date.today()) & ~models.Q(
status=Event.CANCELLED)) | # Active dry hire
(models.Q(dry_hire=True, checked_in_by__isnull=True) & (
models.Q(status=Event.BOOKED) | models.Q(status=Event.CONFIRMED))) | # Active dry hire GT
models.Q(status=Event.CANCELLED, start_date__gte=datetime.date.today()) # Canceled but not started
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person', 'organisation', 'venue', 'mic')
).order_by('start_date', 'end_date', 'start_time', 'end_time', 'meet_at').select_related('person',
'organisation',
'venue', 'mic')
return events
def events_in_bounds(self, start, end):
@@ -259,7 +268,9 @@ class EventManager(models.Manager):
(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
).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
def rig_count(self):
@@ -301,7 +312,8 @@ class Event(models.Model, RevisionMixin):
status = models.IntegerField(choices=EVENT_STATUS_CHOICES, default=PROVISIONAL)
dry_hire = models.BooleanField(default=False)
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
start_date = models.DateField()
@@ -327,6 +339,7 @@ class Event(models.Model, RevisionMixin):
"""
EX Vat
"""
@property
def sum_total(self):
# Manual querying is required for efficiency whilst maintaining floating point arithmetic
@@ -341,7 +354,8 @@ class Event(models.Model, RevisionMixin):
# for item in self.items.filter(cost__gt=0).extra(select="SUM(cost * quantity) AS sum"):
# total += item.sum
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']
if total:
return total
@@ -358,6 +372,7 @@ class Event(models.Model, RevisionMixin):
"""
Inc VAT
"""
@property
def total(self):
return self.sum_total + self.vat
@@ -430,7 +445,6 @@ class Event(models.Model, RevisionMixin):
else:
return endDate
objects = EventManager()
def get_absolute_url(self):
@@ -503,15 +517,6 @@ class Invoice(models.Model):
@property
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']
if total:
return total
@@ -521,6 +526,10 @@ class Invoice(models.Model):
def balance(self):
return self.sum_total - self.payment_total
@property
def is_closed(self):
return self.balance == 0 or self.void
def __str__(self):
return "%i: %s (%.2f)" % (self.pk, self.event, self.balance)

View File

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

View File

@@ -59,6 +59,7 @@
<td>Version ID</td>
<td>User</td>
<td>Changes</td>
<td>Comment</td>
</tr>
</thead>
<tbody>
@@ -71,10 +72,11 @@
<td>{{ version.revision.user.name }}</td>
<td>
{% if version.old == None %}
Object Created
{{version.new|to_class_name}} Created
{% else %}
{% include 'RIGS/version_changes.html' %}
{% endif %} </td>
<td>{{ version.revision.comment }}</td>
</tr>
{% 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>
{% if event.is_rig %}
{% if perms.RIGS.add_invoice %}
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span> <span
class="hidden-xs">Invoice</span></a>
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
{% if event.invoice and event.invoice.is_closed %}
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 %}
</div>
@@ -152,7 +160,7 @@
<a href="{% url 'event_detail' pk=object.based_on.pk %}">
{% if object.based_on.is_rig %}N{{ object.based_on.pk|stringformat:"05d" }}{% else %}
{{ 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>
{% endif %}
</dd>
@@ -191,9 +199,17 @@
class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %}
{% if perms.RIGS.add_invoice %}
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span> <span
class="hidden-xs">Invoice</span></a>
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
{% if event.invoice and event.invoice.is_closed %}
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 %}
</div>
@@ -228,9 +244,17 @@
class="hidden-xs">Duplicate</span></a>
{% if event.is_rig %}
{% if perms.RIGS.add_invoice %}
<a href="{% url 'invoice_event' event.pk %}" class="btn btn-default" title="Invoice Rig"><span
class="glyphicon glyphicon-gbp"></span> <span
class="hidden-xs">Invoice</span></a>
<a id="invoiceDropdownLabel" href="{% url 'invoice_event' event.pk %}" class="btn
{% if event.invoice and event.invoice.is_closed %}
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 %}
</div>

View File

@@ -28,9 +28,9 @@
}
function setTime02Hours() {
var id_start = "{{ form.start_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_start = "{{ form.start_date.id_for_label }}";
var id_end_date = "{{ form.end_date.id_for_label }}";
var id_end_time = "{{ form.end_time.id_for_label }}";
if ($('#'+id_start).val() == $('#'+id_end_date).val()) {
var end_date = new Date($('#'+id_end_date).val());
end_date.setDate(end_date.getDate() + 1);
@@ -62,11 +62,12 @@
} else {
$('.form-is_rig').slideDown();
}
$('.form-hws').css('overflow', 'visible');
} else {
$('#{{form.is_rig.auto_id}}').prop('checked', false);
$('.form-is_rig').slideUp();
}
})
});
{% endif %}
function supportsDate() {
@@ -105,7 +106,7 @@
});
}
})
});
$(document).ready(function () {
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
@@ -149,10 +150,12 @@
<div class="col-md-12 well">
<div class="form-group" id="is_rig-selector">
<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>
</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>
</span>
</div>

View File

@@ -1,59 +1,83 @@
{% extends 'base.html' %}
{% load paginator from filters %}
{% load static %}
{% 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 %}
<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 %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %}
</div>
{% endif %}
<table class="table table-responsive table-hover">
<div class="table-responsive col-sm-12">
<table class="table table-hover">
<thead>
<tr>
<th class="hiddenx-xs">#</th>
<th>Date</th>
<th>Event</th>
<th>Event #</th>
<th>Start Date</th>
<th>Event Name</th>
<th>Client</th>
<th>Cost</th>
<th class="hidden-xs">MIC</th>
<th>MIC</th>
<th></th>
</tr>
</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 #}
{% 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 event.mic %}
{% elif object.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><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">
{{ object.mic.initials }}<br/>
{% 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 %}" target="_blank" class="btn btn-default">
<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>
@@ -61,6 +85,7 @@
{% endfor %}
</tbody>
</table>
</div>
{% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %}

View File

@@ -3,7 +3,7 @@
<div class="table-responsive">
<table class="table">
<thead>
<td class="hidden-xs">#</td>
<td>#</td>
<td>Event Date</td>
<td>Event Details</td>
<td>Event Timings</td>
@@ -25,7 +25,7 @@
danger
{% endif %}
">
<td class="hidden-xs">{{ event.pk }}</td>
<td>{{ event.pk }}</td>
<td>
<div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div>
{% 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="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">
<span class="glyphicon glyphicon-ban-circle"></span> <span
class="hidden-xs">Void</span>
@@ -38,8 +42,11 @@
</div>
</div>
<div class="col-sm-6">
<div class="panel panel-{% if object.void %}danger{% else %}info{% endif %}">
<div class="panel-heading">Event Details</div>
<div class="panel panel-{% if object.is_closed %}success{% else %}warning{% endif %}">
<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">
<dl class="dl-horizontal">
<dt>Event Number</dt>
@@ -109,6 +116,11 @@
</td>
</tr>
{% endfor %}
<tr>
<td class="text-right"><strong>Balance:</strong></td>
<td>{{ object.balance|floatformat:2 }}</td>
<td></td>
<td></td>
</tbody>
</table>
</div>

View File

@@ -5,17 +5,21 @@
{% block content %}
<div class="col-sm-12">
<h2>Invoices</h2>
<h2>{% block heading %}Invoices{% endblock %}</h2>
{% block description %}{% endblock %}
{% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% paginator %}
</div>
{% endif %}
<table class="table table-responsive table-hover">
<div class="table-responsive col-sm-12">
<table class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Invoice #</th>
<th>Event</th>
<th>Client</th>
<th>Event Date</th>
<th>Invoice Date</th>
<th>Balance</th>
<th></th>
@@ -23,9 +27,37 @@
</thead>
<tbody>
{% 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>
<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">
@@ -37,6 +69,7 @@
{% endfor %}
</tbody>
</table>
</div>
{% if is_paginated %}
<div class="col-md-6 col-md-offset-6 col-sm-12 text-right">
{% 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.name }}</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.union_account|yesno|capfirst }}</td>
<td>

View File

@@ -44,7 +44,7 @@
<td>{{ person.pk }}</td>
<td>{{ person.name }}</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>
<a href="{% url 'person_detail' person.pk %}" class="btn btn-default modal-href">

View File

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

View File

@@ -45,7 +45,7 @@
<td>{{ object.pk }}</td>
<td>{{ object.name }}</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>
<a href="{% url 'venue_detail' object.pk %}" class="btn btn-default modal-href">

View File

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

View File

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

View File

@@ -127,6 +127,9 @@ urlpatterns = patterns('',
url(r'^invoice/(?P<pk>\d+)/void/$',
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()),
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/$',
permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
name='payment_create'),

View File

@@ -1,27 +1,18 @@
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.shortcuts import get_object_or_404
from django.views import generic
# Versioning
import reversion
import simplejson
from reversion.models import Version
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 ForeignKey, IntegerField, EmailField, TextField
from django.db.models import IntegerField, EmailField, TextField
from diff_match_patch import diff_match_patch
from RIGS import models, forms
from RIGS import models
import datetime
import re
logger = logging.getLogger('tec.pyrigs')
@@ -33,7 +24,6 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
except AttributeError:
theFields = newObj._meta.fields
class FieldCompare(object):
def __init__(self, field=None, old=None, new=None):
self.field = field
@@ -92,8 +82,15 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
if name in excluded_keys:
continue # if we're excluding this field, skip over it
try:
oldValue = getattr(oldObj, name, None)
except ObjectDoesNotExist:
oldValue = None
try:
newValue = getattr(newObj, name, None)
except ObjectDoesNotExist:
newValue = None
try:
bothBlank = (not oldValue) and (not newValue)
@@ -105,6 +102,7 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
return changes
def compare_event_items(old, new):
# Recieves two event version objects and compares their items, returns an array of ItemCompare objects
@@ -121,10 +119,12 @@ def compare_event_items(old, new):
# Build some dicts of what we have
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
if version.field_dict["event"] == old.object_id_int:
compare = ItemCompare(old=version.object_version.object)
item_dict[version.object_id] = compare
for version in new_item_versions: # go through the new versions
if version.field_dict["event"] == new.object_id_int:
try:
compare = item_dict[version.object_id] # see if there's a matching old version
compare.new = version.object_version.object # then add the new version to the dictionary
@@ -141,6 +141,7 @@ def compare_event_items(old, new):
return changes
def get_versions_for_model(models):
content_types = []
for model in models:
@@ -152,6 +153,7 @@ def get_versions_for_model(models):
return versions
def get_previous_version(version):
thisId = version.object_id
thisVersionId = version.pk
@@ -159,12 +161,14 @@ def get_previous_version(version):
versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId)
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:
return False
return previousVersions
def get_changes_for_version(newVersion, oldVersion=None):
# Pass in a previous version if you already know it (for efficiancy)
# if not provided then it will be looked up in the database
@@ -193,6 +197,7 @@ def get_changes_for_version(newVersion, oldVersion=None):
return compare
class VersionHistory(generic.ListView):
model = reversion.revisions.Version
template_name = "RIGS/version_history.html"
@@ -229,6 +234,7 @@ class VersionHistory(generic.ListView):
return context
class ActivityTable(generic.ListView):
model = reversion.revisions.Version
template_name = "RIGS/activity_table.html"
@@ -239,7 +245,6 @@ class ActivityTable(generic.ListView):
return versions
def get_context_data(self, **kwargs):
# Call the base implementation first to get a context
context = super(ActivityTable, self).get_context_data(**kwargs)
@@ -253,6 +258,7 @@ class ActivityTable(generic.ListView):
return context
class ActivityFeed(generic.ListView):
model = reversion.revisions.Version
template_name = "RIGS/activity_feed_data.html"
@@ -278,7 +284,8 @@ class ActivityFeed(generic.ListView):
if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None:
thisItem['withPrevious'] = False
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
timeTogether = False
for params in maxTimeDelta:
@@ -293,5 +300,4 @@ class ActivityFeed(generic.ListView):
context['object_list'] = items
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
raven==5.8.1
reportlab==3.1.44
selenium==2.53.1
selenium==2.53.6
simplejson==3.7.2
six==1.9.0
sqlparse==0.1.15

View File

@@ -14,7 +14,7 @@
<link rel="icon" type="image/png" 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'>
@@ -74,12 +74,12 @@
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Invoices<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp"></span> Active</a>
</li>
{% if perms.RIGS.add_invoice %}
<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 %}
<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>
Archive</a></li>
</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/*