mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-04 19:18:24 +00:00
Merge master into python-deps
# Conflicts: # PyRIGS/settings.py # RIGS/admin.py # RIGS/models.py # RIGS/test_functional.py # RIGS/urls.py # requirements.txt # wercker.yml
This commit is contained in:
32
.codeclimate.yml
Normal file
32
.codeclimate.yml
Normal 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
6
.coveragerc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[run]
|
||||||
|
source =
|
||||||
|
./
|
||||||
|
|
||||||
|
omit =
|
||||||
|
*/migrations/*
|
||||||
2
.csslintrc
Normal file
2
.csslintrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
--exclude-exts=.min.css
|
||||||
|
--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes
|
||||||
1
.eslintignore
Normal file
1
.eslintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
**/*{.,-}min.js
|
||||||
213
.eslintrc
Normal file
213
.eslintrc
Normal 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
6
.gitignore
vendored
@@ -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
1156
.rubocop.yml
Normal file
File diff suppressed because it is too large
Load Diff
21
.travis.yml
Normal file
21
.travis.yml
Normal 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
|
||||||
@@ -2,23 +2,37 @@ from django.contrib.auth import REDIRECT_FIELD_NAME
|
|||||||
from django.shortcuts import render_to_response
|
from django.shortcuts import render_to_response
|
||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from django.http import HttpResponseRedirect
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
|
||||||
def user_passes_test_with_403(test_func, login_url=None):
|
from RIGS import models
|
||||||
|
|
||||||
|
|
||||||
|
def user_passes_test_with_403(test_func, login_url=None, oembed_view=None):
|
||||||
"""
|
"""
|
||||||
Decorator for views that checks that the user passes the given test.
|
Decorator for views that checks that the user passes the given test.
|
||||||
|
|
||||||
Anonymous users will be redirected to login_url, while users that fail
|
Anonymous users will be redirected to login_url, while users that fail
|
||||||
the test will be given a 403 error.
|
the test will be given a 403 error.
|
||||||
|
If embed_view is set, then a JS redirect will be used, and a application/json+oembed
|
||||||
|
meta tag set with the url of oembed_view
|
||||||
|
(oembed_view will be passed the kwargs from the main function)
|
||||||
"""
|
"""
|
||||||
if not login_url:
|
if not login_url:
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
login_url = settings.LOGIN_URL
|
login_url = settings.LOGIN_URL
|
||||||
|
|
||||||
def _dec(view_func):
|
def _dec(view_func):
|
||||||
def _checklogin(request, *args, **kwargs):
|
def _checklogin(request, *args, **kwargs):
|
||||||
if test_func(request.user):
|
if test_func(request.user):
|
||||||
return view_func(request, *args, **kwargs)
|
return view_func(request, *args, **kwargs)
|
||||||
elif not request.user.is_authenticated():
|
elif not request.user.is_authenticated():
|
||||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
if oembed_view is not None:
|
||||||
|
extra_context = {}
|
||||||
|
extra_context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
|
||||||
|
extra_context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||||
|
resp = render_to_response('login_redirect.html', extra_context, context_instance=RequestContext(request))
|
||||||
|
return resp
|
||||||
|
else:
|
||||||
|
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||||
else:
|
else:
|
||||||
resp = render_to_response('403.html', context_instance=RequestContext(request))
|
resp = render_to_response('403.html', context_instance=RequestContext(request))
|
||||||
resp.status_code = 403
|
resp.status_code = 403
|
||||||
@@ -28,14 +42,14 @@ def user_passes_test_with_403(test_func, login_url=None):
|
|||||||
return _checklogin
|
return _checklogin
|
||||||
return _dec
|
return _dec
|
||||||
|
|
||||||
def permission_required_with_403(perm, login_url=None):
|
|
||||||
|
def permission_required_with_403(perm, login_url=None, oembed_view=None):
|
||||||
"""
|
"""
|
||||||
Decorator for views that checks whether a user has a particular permission
|
Decorator for views that checks whether a user has a particular permission
|
||||||
enabled, redirecting to the log-in page or rendering a 403 as necessary.
|
enabled, redirecting to the log-in page or rendering a 403 as necessary.
|
||||||
"""
|
"""
|
||||||
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url)
|
return user_passes_test_with_403(lambda u: u.has_perm(perm), login_url=login_url, oembed_view=oembed_view)
|
||||||
|
|
||||||
from RIGS import models
|
|
||||||
|
|
||||||
def api_key_required(function):
|
def api_key_required(function):
|
||||||
"""
|
"""
|
||||||
@@ -58,10 +72,10 @@ 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:
|
||||||
return error_resp
|
return error_resp
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
return wrap
|
return wrap
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ 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/
|
||||||
|
|
||||||
@@ -25,8 +23,20 @@ SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get(
|
|||||||
# 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
|
||||||
|
|
||||||
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 = (
|
||||||
@@ -54,6 +64,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',
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -1,5 +1,6 @@
|
|||||||
# TEC PA & Lighting - PyRIGS #
|
# TEC PA & Lighting - PyRIGS #
|
||||||
[](https://app.wercker.com/project/bykey/2dbe0517c3d83859c985ffc5a55a2802)
|
[](https://travis-ci.org/nottinghamtec/PyRIGS)
|
||||||
|
[](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,23 @@ 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.
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ from django.contrib.auth.admin import UserAdmin
|
|||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
from reversion.admin import VersionAdmin
|
from reversion.admin import VersionAdmin
|
||||||
|
|
||||||
|
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, VersionAdmin)
|
admin.site.register(models.Person, VersionAdmin)
|
||||||
admin.site.register(models.Organisation, VersionAdmin)
|
admin.site.register(models.Organisation, VersionAdmin)
|
||||||
@@ -14,15 +22,17 @@ admin.site.register(models.EventItem, 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 +43,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']
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -68,10 +78,11 @@ class InvoicePrint(generic.View):
|
|||||||
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
escapedEventName = re.sub('[^a-zA-Z0-9 \n\.]', '', object.name)
|
||||||
|
|
||||||
response = HttpResponse(content_type='application/pdf')
|
response = HttpResponse(content_type='application/pdf')
|
||||||
response['Content-Disposition'] = "filename=Invoice %05d | %s.pdf" % (invoice.pk, escapedEventName)
|
response['Content-Disposition'] = "filename=Invoice %05d - N%05d | %s.pdf" % (invoice.pk, invoice.event.pk, escapedEventName)
|
||||||
response.write(pdfData)
|
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,9 +94,29 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -94,14 +125,33 @@ class InvoiceWaiting(generic.ListView):
|
|||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
template_name = 'RIGS/event_invoice.html'
|
template_name = 'RIGS/event_invoice.html'
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(InvoiceWaiting, self).get_context_data(**kwargs)
|
||||||
|
total = 0
|
||||||
|
for obj in self.get_objects():
|
||||||
|
total += obj.sum_total
|
||||||
|
context['total'] = total
|
||||||
|
context['count'] = len(self.get_objects())
|
||||||
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
return self.get_objects()
|
||||||
|
|
||||||
|
def get_objects(self):
|
||||||
# @todo find a way to select items
|
# @todo find a way to select items
|
||||||
events = self.model.objects.filter(is_rig=True, end_date__lt=datetime.date.today(),
|
events = self.model.objects.filter(
|
||||||
invoice__isnull=True) \
|
(
|
||||||
.order_by('start_date') \
|
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||||
|
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||||
|
) & Q(invoice__isnull=True) # Has not already been invoiced
|
||||||
|
& Q(is_rig=True) # Is a rig (not non-rig)
|
||||||
|
|
||||||
|
).order_by('start_date') \
|
||||||
.select_related('person',
|
.select_related('person',
|
||||||
'organisation',
|
'organisation',
|
||||||
'venue', 'mic')
|
'venue', 'mic') \
|
||||||
|
.prefetch_related('items')
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -139,4 +190,4 @@ class PaymentDelete(generic.DeleteView):
|
|||||||
model = models.Payment
|
model = models.Payment
|
||||||
|
|
||||||
def get_success_url(self):
|
def get_success_url(self):
|
||||||
return self.request.POST.get('next')
|
return self.request.POST.get('next')
|
||||||
|
|||||||
0
RIGS/management/__init__.py
Normal file
0
RIGS/management/__init__.py
Normal file
0
RIGS/management/commands/__init__.py
Normal file
0
RIGS/management/commands/__init__.py
Normal file
248
RIGS/management/commands/generateSampleData.py
Normal file
248
RIGS/management/commands/generateSampleData.py
Normal 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())
|
||||||
104
RIGS/models.py
104
RIGS/models.py
@@ -1,3 +1,4 @@
|
|||||||
|
import datetime
|
||||||
import hashlib
|
import hashlib
|
||||||
import datetime, pytz
|
import datetime, pytz
|
||||||
|
|
||||||
@@ -9,24 +10,33 @@ from django.utils.functional import cached_property
|
|||||||
from django.utils.encoding import python_2_unicode_compatible
|
from django.utils.encoding import python_2_unicode_compatible
|
||||||
from reversion import revisions as reversion
|
from reversion import revisions as reversion
|
||||||
import string
|
import string
|
||||||
import random
|
|
||||||
from collections import Counter
|
|
||||||
from django.core.urlresolvers import reverse_lazy
|
|
||||||
from django.core.exceptions import ValidationError
|
|
||||||
|
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from collections import Counter
|
||||||
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;
|
||||||
|
|
||||||
@@ -56,6 +66,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):
|
||||||
@@ -80,10 +91,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):
|
||||||
@@ -98,7 +110,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
|
||||||
|
|
||||||
@@ -109,7 +121,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
|
||||||
@@ -142,7 +154,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,8 +164,8 @@ class Organisation(models.Model, RevisionMixin):
|
|||||||
for e in Event.objects.filter(organisation=self).select_related('person'):
|
for e in Event.objects.filter(organisation=self).select_related('person'):
|
||||||
if e.person:
|
if e.person:
|
||||||
p.append(e.person)
|
p.append(e.person)
|
||||||
|
|
||||||
#Count up occurances and put them in descending order
|
# Count up occurances and put them in descending order
|
||||||
c = Counter(p)
|
c = Counter(p)
|
||||||
stats = c.most_common()
|
stats = c.most_common()
|
||||||
return stats
|
return stats
|
||||||
@@ -252,15 +264,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):
|
||||||
@@ -302,7 +316,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()
|
||||||
@@ -328,6 +343,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
|
||||||
@@ -335,14 +351,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
|
||||||
@@ -359,6 +376,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
|
||||||
@@ -383,7 +401,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:
|
||||||
@@ -395,22 +413,22 @@ class Event(models.Model, RevisionMixin):
|
|||||||
# If there is no start time defined, pretend it's midnight
|
# If there is no start time defined, pretend it's midnight
|
||||||
startTimeFaked = False
|
startTimeFaked = False
|
||||||
if self.has_start_time:
|
if self.has_start_time:
|
||||||
startDateTime = datetime.datetime.combine(self.start_date,self.start_time)
|
startDateTime = datetime.datetime.combine(self.start_date, self.start_time)
|
||||||
else:
|
else:
|
||||||
startDateTime = datetime.datetime.combine(self.start_date,datetime.time(00,00))
|
startDateTime = datetime.datetime.combine(self.start_date, datetime.time(00, 00))
|
||||||
startTimeFaked = True
|
startTimeFaked = True
|
||||||
|
|
||||||
#timezoneIssues - apply the default timezone to the naiive datetime
|
# timezoneIssues - apply the default timezone to the naiive datetime
|
||||||
tz = pytz.timezone(settings.TIME_ZONE)
|
tz = pytz.timezone(settings.TIME_ZONE)
|
||||||
startDateTime = tz.localize(startDateTime)
|
startDateTime = tz.localize(startDateTime)
|
||||||
datetime_list.append(startDateTime) # then add it to the list
|
datetime_list.append(startDateTime) # then add it to the list
|
||||||
|
|
||||||
earliest = min(datetime_list).astimezone(tz) #find the earliest datetime in the list
|
earliest = min(datetime_list).astimezone(tz) # find the earliest datetime in the list
|
||||||
|
|
||||||
# if we faked it & it's the earliest, better own up
|
# if we faked it & it's the earliest, better own up
|
||||||
if startTimeFaked and earliest==startDateTime:
|
if startTimeFaked and earliest == startDateTime:
|
||||||
return self.start_date
|
return self.start_date
|
||||||
|
|
||||||
return earliest
|
return earliest
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -422,7 +440,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)
|
||||||
|
|
||||||
@@ -431,7 +449,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):
|
||||||
@@ -447,7 +464,7 @@ class Event(models.Model, RevisionMixin):
|
|||||||
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
startEndSameDay = not self.end_date or self.end_date == self.start_date
|
||||||
hasStartAndEnd = self.has_start_time and self.has_end_time
|
hasStartAndEnd = self.has_start_time and self.has_end_time
|
||||||
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
if startEndSameDay and hasStartAndEnd and self.start_time > self.end_time:
|
||||||
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
|
raise ValidationError('Unless you\'ve invented time travel, the event can\'t finish before it has started.')
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
"""Call :meth:`full_clean` before saving."""
|
"""Call :meth:`full_clean` before saving."""
|
||||||
@@ -504,15 +521,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
|
||||||
@@ -522,6 +530,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)
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,13 @@ from django.shortcuts import get_object_or_404
|
|||||||
from django.template import RequestContext
|
from django.template import RequestContext
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from z3c.rml import rml2pdf
|
from z3c.rml import rml2pdf
|
||||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||||
|
import simplejson
|
||||||
|
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
import datetime
|
import datetime
|
||||||
@@ -47,6 +49,29 @@ class EventDetail(generic.DetailView):
|
|||||||
model = models.Event
|
model = models.Event
|
||||||
|
|
||||||
|
|
||||||
|
class EventOembed(generic.View):
|
||||||
|
model = models.Event
|
||||||
|
|
||||||
|
def get(self, request, pk=None):
|
||||||
|
|
||||||
|
embed_url = reverse('event_embed', args=[pk])
|
||||||
|
full_url = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], embed_url)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'html': '<iframe src="{0}" frameborder="0" width="100%" height="250"></iframe>'.format(full_url),
|
||||||
|
'version': '1.0',
|
||||||
|
'type': 'rich',
|
||||||
|
'height': '250'
|
||||||
|
}
|
||||||
|
|
||||||
|
json = simplejson.JSONEncoderForHTML().encode(data)
|
||||||
|
return HttpResponse(json, content_type="application/json")
|
||||||
|
|
||||||
|
|
||||||
|
class EventEmbed(EventDetail):
|
||||||
|
template_name = 'RIGS/event_embed.html'
|
||||||
|
|
||||||
|
|
||||||
class EventCreate(generic.CreateView):
|
class EventCreate(generic.CreateView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
form_class = forms.EventForm
|
form_class = forms.EventForm
|
||||||
@@ -59,7 +84,7 @@ class EventCreate(generic.CreateView):
|
|||||||
form = context['form']
|
form = context['form']
|
||||||
if re.search('"-\d+"', form['items_json'].value()):
|
if re.search('"-\d+"', form['items_json'].value()):
|
||||||
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
||||||
|
|
||||||
|
|
||||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||||
for field, model in form.related_models.iteritems():
|
for field, model in form.related_models.iteritems():
|
||||||
@@ -97,11 +122,12 @@ class EventDuplicate(EventUpdate):
|
|||||||
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
|
old = super(EventDuplicate, self).get_object(queryset) # Get the object (the event you're duplicating)
|
||||||
new = copy.copy(old) # Make a copy of the object in memory
|
new = copy.copy(old) # Make a copy of the object in memory
|
||||||
new.based_on = old # Make the new event based on the old event
|
new.based_on = old # Make the new event based on the old event
|
||||||
|
new.purchase_order = None
|
||||||
|
|
||||||
if self.request.method in ('POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
|
if self.request.method in ('POST', 'PUT'): # This only happens on save (otherwise items won't display in editor)
|
||||||
new.pk = None # This means a new event will be created on save, and all items will be re-created
|
new.pk = None # This means a new event will be created on save, and all items will be re-created
|
||||||
|
else:
|
||||||
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
||||||
|
|
||||||
return new
|
return new
|
||||||
|
|
||||||
@@ -192,4 +218,4 @@ class EventArchive(generic.ArchiveIndexView):
|
|||||||
if len(qs) == 0:
|
if len(qs) == 0:
|
||||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||||
|
|
||||||
return qs
|
return qs
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -147,3 +147,45 @@ ins {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.embedded{
|
||||||
|
min-height:100%;
|
||||||
|
display: table;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
body{
|
||||||
|
padding:0;
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
width:100%;
|
||||||
|
background:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embed_container{
|
||||||
|
border:5px solid #e9e9e9;
|
||||||
|
padding:12px 0px;
|
||||||
|
min-height:100%;
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source{
|
||||||
|
background: url('/static/imgs/pyrigs-avatar.png') no-repeat;
|
||||||
|
background-size: 16px 16px;
|
||||||
|
padding-left: 20px;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3{
|
||||||
|
margin-top:10px;
|
||||||
|
margin-bottom:5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
p{
|
||||||
|
margin-bottom:2px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-mic-photo{
|
||||||
|
max-width: 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
40
RIGS/templates/RIGS/admin_associate_merge.html
Normal file
40
RIGS/templates/RIGS/admin_associate_merge.html
Normal 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 %}
|
||||||
@@ -25,9 +25,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>
|
||||||
@@ -151,7 +159,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>
|
||||||
@@ -190,9 +198,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>
|
||||||
@@ -227,9 +243,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>
|
||||||
|
|||||||
106
RIGS/templates/RIGS/event_embed.html
Normal file
106
RIGS/templates/RIGS/event_embed.html
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
{% extends 'base_embed.html' %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<a href="/">
|
||||||
|
<span class="source"> R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12">
|
||||||
|
<span class="pull-right">
|
||||||
|
{% if object.mic %}
|
||||||
|
<div class="text-center">
|
||||||
|
<img src="{{ object.mic.profile_picture }}" class="event-mic-photo img-rounded"/>
|
||||||
|
</div>
|
||||||
|
{% elif object.is_rig %}
|
||||||
|
<span class="glyphicon glyphicon-exclamation-sign"></span>
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
<a {% if perms.RIGS.view_event %}href="{% url 'event_detail' object.pk %}"{% endif %}>
|
||||||
|
{% if object.is_rig %}N{{ object.pk|stringformat:"05d" }}{% else %}{{ object.pk }}{% endif %}
|
||||||
|
| {{ object.name }} </a>
|
||||||
|
{% if object.venue %}
|
||||||
|
<small>at {{ object.venue }}</small>
|
||||||
|
{% endif %}
|
||||||
|
<br/><small>
|
||||||
|
{{ object.start_date|date:"D d/m/Y" }}
|
||||||
|
{% if object.has_start_time %}
|
||||||
|
{{ object.start_time|date:"H:i" }}
|
||||||
|
{% endif %}
|
||||||
|
{% if object.end_date or object.has_end_time %}
|
||||||
|
–
|
||||||
|
{% endif %}
|
||||||
|
{% if object.end_date and object.end_date != object.start_date %}
|
||||||
|
{{ object.end_date|date:"D d/m/Y" }}
|
||||||
|
{% endif %}
|
||||||
|
{% if object.has_end_time %}
|
||||||
|
{{ object.end_time|date:"H:i" }}
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-6">
|
||||||
|
<p>
|
||||||
|
<strong>Status:</strong>
|
||||||
|
{{ object.get_status_display }}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{% if object.is_rig %}
|
||||||
|
<strong>Client:</strong> {{ object.person.name }}
|
||||||
|
{% if object.organisation %}
|
||||||
|
for {{ object.organisation.name }}
|
||||||
|
{% endif %}
|
||||||
|
{% if object.dry_hire %}(Dry Hire){% endif %}
|
||||||
|
{% else %}
|
||||||
|
<strong>Non-Rig</strong>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>MIC:</strong>
|
||||||
|
{% if object.mic %}
|
||||||
|
{{object.mic.name}}
|
||||||
|
{% else %}
|
||||||
|
None
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-6">
|
||||||
|
|
||||||
|
{% if object.meet_at %}
|
||||||
|
<p>
|
||||||
|
<strong>Crew meet:</strong>
|
||||||
|
{{ object.meet_at|date:"H:i" }} {{ object.meet_at|date:"(Y-m-d)" }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
{% if object.access_at %}
|
||||||
|
<p>
|
||||||
|
<strong>Access at:</strong>
|
||||||
|
{{ object.access_at|date:"H:i" }} {{ object.access_at|date:"(Y-m-d)" }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
<strong>Last updated:</strong>
|
||||||
|
{{ object.last_edited_at }} by "{{ object.last_edited_by.initials }}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if object.description %}
|
||||||
|
<p>
|
||||||
|
<strong>Description: </strong>
|
||||||
|
{{ object.description|linebreaksbr }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -29,9 +29,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setTime02Hours() {
|
function setTime02Hours() {
|
||||||
var id_start = "{{ form.start_date.id_for_label }}"
|
var id_start = "{{ form.start_date.id_for_label }}";
|
||||||
var id_end_date = "{{ form.end_date.id_for_label }}"
|
var id_end_date = "{{ form.end_date.id_for_label }}";
|
||||||
var id_end_time = "{{ form.end_time.id_for_label }}"
|
var id_end_time = "{{ form.end_time.id_for_label }}";
|
||||||
if ($('#'+id_start).val() == $('#'+id_end_date).val()) {
|
if ($('#'+id_start).val() == $('#'+id_end_date).val()) {
|
||||||
var end_date = new Date($('#'+id_end_date).val());
|
var end_date = new Date($('#'+id_end_date).val());
|
||||||
end_date.setDate(end_date.getDate() + 1);
|
end_date.setDate(end_date.getDate() + 1);
|
||||||
@@ -63,11 +63,12 @@
|
|||||||
} else {
|
} else {
|
||||||
$('.form-is_rig').slideDown();
|
$('.form-is_rig').slideDown();
|
||||||
}
|
}
|
||||||
|
$('.form-hws, .form-hws .form-is_rig').css('overflow', 'visible');
|
||||||
} else {
|
} else {
|
||||||
$('#{{form.is_rig.auto_id}}').prop('checked', false);
|
$('#{{form.is_rig.auto_id}}').prop('checked', false);
|
||||||
$('.form-is_rig').slideUp();
|
$('.form-is_rig').slideUp();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
function supportsDate() {
|
function supportsDate() {
|
||||||
@@ -106,7 +107,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
|
setupItemTable($("#{{ form.items_json.id_for_label }}").val());
|
||||||
@@ -150,10 +151,12 @@
|
|||||||
<div class="col-md-12 well">
|
<div class="col-md-12 well">
|
||||||
<div class="form-group" id="is_rig-selector">
|
<div class="form-group" id="is_rig-selector">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<span class="col-sm-6">
|
<span class="col-sm-6" data-toggle="tooltip"
|
||||||
|
title="Anything that involves TEC kit, crew, or otherwise us providing a service to anyone.">
|
||||||
<button type="button" class="btn btn-primary col-xs-12" data-is_rig="1">Rig</button>
|
<button type="button" class="btn btn-primary col-xs-12" data-is_rig="1">Rig</button>
|
||||||
</span>
|
</span>
|
||||||
<span class="col-sm-6">
|
<span class="col-sm-6" data-toggle="tooltip"
|
||||||
|
title="Things that aren't service-based, like training, meetings and site visits.">
|
||||||
<button type="button" class="btn btn-info col-xs-12" data-is_rig="0">Non-Rig</button>
|
<button type="button" class="btn btn-info col-xs-12" data-is_rig="0">Non-Rig</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -442,4 +445,4 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% include 'RIGS/item_modal.html' %}
|
{% include 'RIGS/item_modal.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<td class="hidden-xs">#</td>
|
<td>#</td>
|
||||||
<td>Event Date</td>
|
<td>Event Date</td>
|
||||||
<td>Event Details</td>
|
<td>Event Details</td>
|
||||||
<td>Event Timings</td>
|
<td>Event Timings</td>
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
danger
|
danger
|
||||||
{% endif %}
|
{% endif %}
|
||||||
">
|
">
|
||||||
<td class="hidden-xs">{{ event.pk }}</td>
|
<td>{{ event.pk }}</td>
|
||||||
<td>
|
<td>
|
||||||
<div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div>
|
<div><strong>{{ event.start_date|date:"D d/m/Y" }}</strong></div>
|
||||||
{% if event.end_date and event.end_date != event.start_date %}
|
{% if event.end_date and event.end_date != event.start_date %}
|
||||||
|
|||||||
@@ -23,9 +23,11 @@
|
|||||||
|
|
||||||
<div class="list-group-item default"></div>
|
<div class="list-group-item default"></div>
|
||||||
|
|
||||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/forum" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
|
<a class="list-group-item" href="https://forum.nottinghamtec.co.uk" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
|
||||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
|
<a class="list-group-item" href="//members.nottinghamtec.co.uk/wiki" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Wiki</a>
|
||||||
|
<a class="list-group-item" href="http://members.nottinghamtec.co.uk/wiki/images/2/22/Event_Risk_Assesment.pdf" target="_blank"><span class="glyphicon glyphicon-link"></span> Pre-Event Risk Assessment</a>
|
||||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a>
|
<a class="list-group-item" href="//members.nottinghamtec.co.uk/price" target="_blank"><span class="glyphicon glyphicon-link"></span> Price List</a>
|
||||||
|
<a class="list-group-item" href="https://form.jotformeu.com/62203600438344" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,4 +77,4 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
35
RIGS/templates/RIGS/invoice_confirm_delete.html
Normal file
35
RIGS/templates/RIGS/invoice_confirm_delete.html
Normal 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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
13
RIGS/templates/RIGS/invoice_list_active.html
Normal file
13
RIGS/templates/RIGS/invoice_list_active.html
Normal 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 %}
|
||||||
13
RIGS/templates/RIGS/invoice_list_archive.html
Normal file
13
RIGS/templates/RIGS/invoice_list_archive.html
Normal 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 %}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
# -*- 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 re
|
||||||
import os
|
import os
|
||||||
from datetime import date, timedelta
|
from datetime import date, timedelta
|
||||||
@@ -15,6 +23,7 @@ from reversion import revisions as reversion
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class UserRegistrationTest(LiveServerTestCase):
|
class UserRegistrationTest(LiveServerTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -103,7 +112,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)
|
||||||
@@ -437,7 +446,7 @@ class EventTest(LiveServerTestCase):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def testEventDuplicate(self):
|
def testEventDuplicate(self):
|
||||||
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end")
|
testEvent = models.Event.objects.create(name="TE E1", status=models.Event.PROVISIONAL, start_date=date.today() + timedelta(days=6), description="start future no end", purchase_order="TESTPO")
|
||||||
|
|
||||||
item1 = models.EventItem(
|
item1 = models.EventItem(
|
||||||
event=testEvent,
|
event=testEvent,
|
||||||
@@ -470,6 +479,9 @@ class EventTest(LiveServerTestCase):
|
|||||||
self.assertIn("Test Item 1", table.text)
|
self.assertIn("Test Item 1", table.text)
|
||||||
self.assertIn("Test Item 2", table.text)
|
self.assertIn("Test Item 2", table.text)
|
||||||
|
|
||||||
|
# Check the info message is visible
|
||||||
|
self.assertIn("Event data duplicated but not yet saved",self.browser.find_element_by_id('content').text)
|
||||||
|
|
||||||
# Add item
|
# Add item
|
||||||
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
||||||
wait.until(animation_is_finished())
|
wait.until(animation_is_finished())
|
||||||
@@ -487,7 +499,12 @@ class EventTest(LiveServerTestCase):
|
|||||||
# Attempt to save
|
# Attempt to save
|
||||||
save.click()
|
save.click()
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
self.assertNotIn("N%05d"%testEvent.pk, self.browser.find_element_by_xpath('//h1').text)
|
self.assertNotIn("N%05d"%testEvent.pk, self.browser.find_element_by_xpath('//h1').text)
|
||||||
|
=======
|
||||||
|
self.assertNotIn("N0000%d"%testEvent.pk, self.browser.find_element_by_xpath('//h1').text)
|
||||||
|
self.assertNotIn("Event data duplicated but not yet saved", self.browser.find_element_by_id('content').text) # Check info message not visible
|
||||||
|
>>>>>>> 9b7c84cf0890788a08a3dec71e00cbe78748b1fb
|
||||||
|
|
||||||
# Check the new items are visible
|
# Check the new items are visible
|
||||||
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
||||||
@@ -496,7 +513,13 @@ class EventTest(LiveServerTestCase):
|
|||||||
self.assertIn("Test Item 3", table.text)
|
self.assertIn("Test Item 3", table.text)
|
||||||
|
|
||||||
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
||||||
|
<<<<<<< HEAD
|
||||||
self.assertIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
self.assertIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
=======
|
||||||
|
self.assertIn("N0000%d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
# Check the PO hasn't carried through
|
||||||
|
self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
|
||||||
|
>>>>>>> 9b7c84cf0890788a08a3dec71e00cbe78748b1fb
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -504,7 +527,13 @@ class EventTest(LiveServerTestCase):
|
|||||||
|
|
||||||
#Check that based-on hasn't crept into the old event
|
#Check that based-on hasn't crept into the old event
|
||||||
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
||||||
|
<<<<<<< HEAD
|
||||||
self.assertNotIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
self.assertNotIn("N%05d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
=======
|
||||||
|
self.assertNotIn("N0000%d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
||||||
|
# Check the PO remains on the old event
|
||||||
|
self.assertIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
|
||||||
|
>>>>>>> 9b7c84cf0890788a08a3dec71e00cbe78748b1fb
|
||||||
|
|
||||||
# Check the items are as they were
|
# Check the items are as they were
|
||||||
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
||||||
|
|||||||
310
RIGS/test_unit.py
Normal file
310
RIGS/test_unit.py
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
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 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 TestEmbeddedViews(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()
|
||||||
|
|
||||||
|
def testLoginRedirect(self):
|
||||||
|
request_url = reverse('event_embed', kwargs={'pk': 1})
|
||||||
|
expected_url = "{0}?next={1}".format(reverse('login_embed'), request_url)
|
||||||
|
|
||||||
|
# Request the page and check it redirects
|
||||||
|
response = self.client.get(request_url, follow=True)
|
||||||
|
self.assertRedirects(response, expected_url, status_code=302, target_status_code=200)
|
||||||
|
|
||||||
|
# Now login
|
||||||
|
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||||
|
|
||||||
|
# And check that it no longer redirects
|
||||||
|
response = self.client.get(request_url, follow=True)
|
||||||
|
self.assertEqual(len(response.redirect_chain), 0)
|
||||||
|
|
||||||
|
def testLoginCookieWarning(self):
|
||||||
|
login_url = reverse('login_embed')
|
||||||
|
response = self.client.post(login_url, follow=True)
|
||||||
|
self.assertContains(response, "Cookies do not seem to be enabled")
|
||||||
|
|
||||||
|
def testXFrameHeaders(self):
|
||||||
|
event_url = reverse('event_embed', kwargs={'pk': 1})
|
||||||
|
login_url = reverse('login_embed')
|
||||||
|
|
||||||
|
self.assertTrue(self.client.login(username=self.profile.username, password='testuser'))
|
||||||
|
|
||||||
|
response = self.client.get(event_url, follow=True)
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
response._headers["X-Frame-Options"]
|
||||||
|
|
||||||
|
response = self.client.get(login_url, follow=True)
|
||||||
|
with self.assertRaises(KeyError):
|
||||||
|
response._headers["X-Frame-Options"]
|
||||||
|
|
||||||
|
def testOEmbed(self):
|
||||||
|
event_url = reverse('event_detail', kwargs={'pk': 1})
|
||||||
|
event_embed_url = reverse('event_embed', kwargs={'pk': 1})
|
||||||
|
oembed_url = reverse('event_oembed', kwargs={'pk': 1})
|
||||||
|
|
||||||
|
alt_oembed_url = reverse('event_oembed', kwargs={'pk': 999})
|
||||||
|
alt_event_embed_url = reverse('event_embed', kwargs={'pk': 999})
|
||||||
|
|
||||||
|
# Test the meta tag is in place
|
||||||
|
response = self.client.get(event_url, follow=True, HTTP_HOST='example.com')
|
||||||
|
self.assertContains(response, '<link rel="alternate" type="application/json+oembed"')
|
||||||
|
self.assertContains(response, oembed_url)
|
||||||
|
|
||||||
|
# Test that the JSON exists
|
||||||
|
response = self.client.get(oembed_url, follow=True, HTTP_HOST='example.com')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, event_embed_url)
|
||||||
|
|
||||||
|
# Should also work for non-existant events
|
||||||
|
response = self.client.get(alt_oembed_url, follow=True, HTTP_HOST='example.com')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertContains(response, alt_event_embed_url)
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
282
RIGS/urls.py
282
RIGS/urls.py
@@ -1,158 +1,170 @@
|
|||||||
from django.conf.urls import patterns, include, url
|
from django.conf.urls import patterns, include, url
|
||||||
from django.contrib.auth.views import password_reset
|
from django.contrib.auth.views import password_reset
|
||||||
|
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from RIGS import models, views, rigboard, finance, ical, versioning, forms
|
from RIGS import models, views, rigboard, finance, ical, versioning, forms
|
||||||
from django.views.generic import RedirectView
|
from django.views.generic import RedirectView
|
||||||
|
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||||
|
|
||||||
from PyRIGS.decorators import permission_required_with_403
|
from PyRIGS.decorators import permission_required_with_403
|
||||||
from PyRIGS.decorators import api_key_required
|
from PyRIGS.decorators import api_key_required
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = patterns('',
|
||||||
# Examples:
|
# Examples:
|
||||||
# url(r'^$', 'PyRIGS.views.home', name='home'),
|
# url(r'^$', 'PyRIGS.views.home', name='home'),
|
||||||
# url(r'^blog/', include('blog.urls')),
|
# url(r'^blog/', include('blog.urls')),
|
||||||
url('^$', login_required(views.Index.as_view()), name='index'),
|
url('^$', login_required(views.Index.as_view()), name='index'),
|
||||||
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
||||||
|
|
||||||
url('^user/login/$', views.login, name='login'),
|
url('^user/login/$', 'RIGS.views.login', name='login'),
|
||||||
url(r'^user/password_reset/$', password_reset, {'password_reset_form': forms.PasswordReset}),
|
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
|
||||||
|
url(r'^user/password_reset/$', 'django.contrib.auth.views.password_reset', {'password_reset_form': forms.PasswordReset}),
|
||||||
|
|
||||||
# People
|
# People
|
||||||
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
|
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
|
||||||
name='person_list'),
|
name='person_list'),
|
||||||
url(r'^people/add/$',
|
url(r'^people/add/$',
|
||||||
permission_required_with_403('RIGS.add_person')(views.PersonCreate.as_view()),
|
permission_required_with_403('RIGS.add_person')(views.PersonCreate.as_view()),
|
||||||
name='person_create'),
|
name='person_create'),
|
||||||
url(r'^people/(?P<pk>\d+)/$',
|
url(r'^people/(?P<pk>\d+)/$',
|
||||||
permission_required_with_403('RIGS.view_person')(views.PersonDetail.as_view()),
|
permission_required_with_403('RIGS.view_person')(views.PersonDetail.as_view()),
|
||||||
name='person_detail'),
|
name='person_detail'),
|
||||||
url(r'^people/(?P<pk>\d+)/history/$',
|
url(r'^people/(?P<pk>\d+)/history/$',
|
||||||
permission_required_with_403('RIGS.view_person')(versioning.VersionHistory.as_view()),
|
permission_required_with_403('RIGS.view_person')(versioning.VersionHistory.as_view()),
|
||||||
name='person_history', kwargs={'model': models.Person}),
|
name='person_history', kwargs={'model': models.Person}),
|
||||||
url(r'^people/(?P<pk>\d+)/edit/$',
|
url(r'^people/(?P<pk>\d+)/edit/$',
|
||||||
permission_required_with_403('RIGS.change_person')(views.PersonUpdate.as_view()),
|
permission_required_with_403('RIGS.change_person')(views.PersonUpdate.as_view()),
|
||||||
name='person_update'),
|
name='person_update'),
|
||||||
|
|
||||||
# Organisations
|
# Organisations
|
||||||
url(r'^organisations/$',
|
url(r'^organisations/$',
|
||||||
permission_required_with_403('RIGS.view_organisation')(views.OrganisationList.as_view()),
|
permission_required_with_403('RIGS.view_organisation')(views.OrganisationList.as_view()),
|
||||||
name='organisation_list'),
|
name='organisation_list'),
|
||||||
url(r'^organisations/add/$',
|
url(r'^organisations/add/$',
|
||||||
permission_required_with_403('RIGS.add_organisation')(views.OrganisationCreate.as_view()),
|
permission_required_with_403('RIGS.add_organisation')(views.OrganisationCreate.as_view()),
|
||||||
name='organisation_create'),
|
name='organisation_create'),
|
||||||
url(r'^organisations/(?P<pk>\d+)/$',
|
url(r'^organisations/(?P<pk>\d+)/$',
|
||||||
permission_required_with_403('RIGS.view_organisation')(views.OrganisationDetail.as_view()),
|
permission_required_with_403('RIGS.view_organisation')(views.OrganisationDetail.as_view()),
|
||||||
name='organisation_detail'),
|
name='organisation_detail'),
|
||||||
url(r'^organisations/(?P<pk>\d+)/history/$',
|
url(r'^organisations/(?P<pk>\d+)/history/$',
|
||||||
permission_required_with_403('RIGS.view_organisation')(versioning.VersionHistory.as_view()),
|
permission_required_with_403('RIGS.view_organisation')(versioning.VersionHistory.as_view()),
|
||||||
name='organisation_history', kwargs={'model': models.Organisation}),
|
name='organisation_history', kwargs={'model': models.Organisation}),
|
||||||
url(r'^organisations/(?P<pk>\d+)/edit/$',
|
url(r'^organisations/(?P<pk>\d+)/edit/$',
|
||||||
permission_required_with_403('RIGS.change_organisation')(views.OrganisationUpdate.as_view()),
|
permission_required_with_403('RIGS.change_organisation')(views.OrganisationUpdate.as_view()),
|
||||||
name='organisation_update'),
|
name='organisation_update'),
|
||||||
|
|
||||||
# Venues
|
# Venues
|
||||||
url(r'^venues/$',
|
url(r'^venues/$',
|
||||||
permission_required_with_403('RIGS.view_venue')(views.VenueList.as_view()),
|
permission_required_with_403('RIGS.view_venue')(views.VenueList.as_view()),
|
||||||
name='venue_list'),
|
name='venue_list'),
|
||||||
url(r'^venues/add/$',
|
url(r'^venues/add/$',
|
||||||
permission_required_with_403('RIGS.add_venue')(views.VenueCreate.as_view()),
|
permission_required_with_403('RIGS.add_venue')(views.VenueCreate.as_view()),
|
||||||
name='venue_create'),
|
name='venue_create'),
|
||||||
url(r'^venues/(?P<pk>\d+)/$',
|
url(r'^venues/(?P<pk>\d+)/$',
|
||||||
permission_required_with_403('RIGS.view_venue')(views.VenueDetail.as_view()),
|
permission_required_with_403('RIGS.view_venue')(views.VenueDetail.as_view()),
|
||||||
name='venue_detail'),
|
name='venue_detail'),
|
||||||
url(r'^venues/(?P<pk>\d+)/history/$',
|
url(r'^venues/(?P<pk>\d+)/history/$',
|
||||||
permission_required_with_403('RIGS.view_venue')(versioning.VersionHistory.as_view()),
|
permission_required_with_403('RIGS.view_venue')(versioning.VersionHistory.as_view()),
|
||||||
name='venue_history', kwargs={'model': models.Venue}),
|
name='venue_history', kwargs={'model': models.Venue}),
|
||||||
url(r'^venues/(?P<pk>\d+)/edit/$',
|
url(r'^venues/(?P<pk>\d+)/edit/$',
|
||||||
permission_required_with_403('RIGS.change_venue')(views.VenueUpdate.as_view()),
|
permission_required_with_403('RIGS.change_venue')(views.VenueUpdate.as_view()),
|
||||||
name='venue_update'),
|
name='venue_update'),
|
||||||
|
|
||||||
# Rigboard
|
# Rigboard
|
||||||
url(r'^rigboard/$', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
|
url(r'^rigboard/$', login_required(rigboard.RigboardIndex.as_view()), name='rigboard'),
|
||||||
url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()),
|
url(r'^rigboard/calendar/(?P<view>(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||||
name='web_calendar'),
|
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$',
|
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||||
login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
url(r'^rigboard/activity/$',
|
||||||
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()),
|
||||||
url(r'^rigboard/activity/$',
|
name='activity_table'),
|
||||||
permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()),
|
url(r'^rigboard/activity/feed/$',
|
||||||
name='activity_table'),
|
permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()),
|
||||||
url(r'^rigboard/activity/feed/$',
|
name='activity_feed'),
|
||||||
permission_required_with_403('RIGS.view_event')(versioning.ActivityFeed.as_view()),
|
|
||||||
name='activity_feed'),
|
|
||||||
|
|
||||||
url(r'^event/(?P<pk>\d+)/$',
|
url(r'^event/(?P<pk>\d+)/$',
|
||||||
permission_required_with_403('RIGS.view_event')(rigboard.EventDetail.as_view()),
|
permission_required_with_403('RIGS.view_event', oembed_view="event_oembed")(rigboard.EventDetail.as_view()),
|
||||||
name='event_detail'),
|
name='event_detail'),
|
||||||
url(r'^event/(?P<pk>\d+)/print/$',
|
url(r'^event/(?P<pk>\d+)/embed/$',
|
||||||
permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
|
xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
|
||||||
name='event_print'),
|
name='event_embed'),
|
||||||
url(r'^event/create/$',
|
url(r'^event/(?P<pk>\d+)/oembed_json/$',
|
||||||
permission_required_with_403('RIGS.add_event')(rigboard.EventCreate.as_view()),
|
rigboard.EventOembed.as_view(),
|
||||||
name='event_create'),
|
name='event_oembed'),
|
||||||
url(r'^event/(?P<pk>\d+)/edit/$',
|
url(r'^event/(?P<pk>\d+)/print/$',
|
||||||
permission_required_with_403('RIGS.change_event')(rigboard.EventUpdate.as_view()),
|
permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
|
||||||
name='event_update'),
|
name='event_print'),
|
||||||
url(r'^event/(?P<pk>\d+)/duplicate/$',
|
url(r'^event/create/$',
|
||||||
permission_required_with_403('RIGS.add_event')(rigboard.EventDuplicate.as_view()),
|
permission_required_with_403('RIGS.add_event')(rigboard.EventCreate.as_view()),
|
||||||
name='event_duplicate'),
|
name='event_create'),
|
||||||
url(r'^event/archive/$', login_required()(rigboard.EventArchive.as_view()),
|
url(r'^event/(?P<pk>\d+)/edit/$',
|
||||||
name='event_archive'),
|
permission_required_with_403('RIGS.change_event')(rigboard.EventUpdate.as_view()),
|
||||||
|
name='event_update'),
|
||||||
|
url(r'^event/(?P<pk>\d+)/duplicate/$',
|
||||||
|
permission_required_with_403('RIGS.add_event')(rigboard.EventDuplicate.as_view()),
|
||||||
|
name='event_duplicate'),
|
||||||
|
url(r'^event/archive/$', login_required()(rigboard.EventArchive.as_view()),
|
||||||
|
name='event_archive'),
|
||||||
|
|
||||||
url(r'^event/(?P<pk>\d+)/history/$',
|
url(r'^event/(?P<pk>\d+)/history/$',
|
||||||
permission_required_with_403('RIGS.view_event')(versioning.VersionHistory.as_view()),
|
permission_required_with_403('RIGS.view_event')(versioning.VersionHistory.as_view()),
|
||||||
name='event_history', kwargs={'model': models.Event}),
|
name='event_history', kwargs={'model': models.Event}),
|
||||||
|
|
||||||
# Finance
|
|
||||||
url(r'^invoice/$',
|
|
||||||
permission_required_with_403('RIGS.view_invoice')(finance.InvoiceIndex.as_view()),
|
|
||||||
name='invoice_list'),
|
|
||||||
url(r'^invoice/archive/$',
|
|
||||||
permission_required_with_403('RIGS.view_invoice')(finance.InvoiceArchive.as_view()),
|
|
||||||
name='invoice_archive'),
|
|
||||||
url(r'^invoice/waiting/$',
|
|
||||||
permission_required_with_403('RIGS.add_invoice')(finance.InvoiceWaiting.as_view()),
|
|
||||||
name='invoice_waiting'),
|
|
||||||
|
|
||||||
url(r'^event/(?P<pk>\d+)/invoice/$',
|
|
||||||
permission_required_with_403('RIGS.add_invoice')(finance.InvoiceEvent.as_view()),
|
|
||||||
name='invoice_event'),
|
|
||||||
|
|
||||||
url(r'^invoice/(?P<pk>\d+)/$',
|
# Finance
|
||||||
permission_required_with_403('RIGS.view_invoice')(finance.InvoiceDetail.as_view()),
|
url(r'^invoice/$',
|
||||||
name='invoice_detail'),
|
permission_required_with_403('RIGS.view_invoice')(finance.InvoiceIndex.as_view()),
|
||||||
url(r'^invoice/(?P<pk>\d+)/print/$',
|
name='invoice_list'),
|
||||||
permission_required_with_403('RIGS.view_invoice')(finance.InvoicePrint.as_view()),
|
url(r'^invoice/archive/$',
|
||||||
name='invoice_print'),
|
permission_required_with_403('RIGS.view_invoice')(finance.InvoiceArchive.as_view()),
|
||||||
url(r'^invoice/(?P<pk>\d+)/void/$',
|
name='invoice_archive'),
|
||||||
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()),
|
url(r'^invoice/waiting/$',
|
||||||
name='invoice_void'),
|
permission_required_with_403('RIGS.add_invoice')(finance.InvoiceWaiting.as_view()),
|
||||||
url(r'^payment/create/$',
|
name='invoice_waiting'),
|
||||||
permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
|
|
||||||
name='payment_create'),
|
|
||||||
url(r'^payment/(?P<pk>\d+)/delete/$',
|
|
||||||
permission_required_with_403('RIGS.add_payment')(finance.PaymentDelete.as_view()),
|
|
||||||
name='payment_delete'),
|
|
||||||
|
|
||||||
# User editing
|
url(r'^event/(?P<pk>\d+)/invoice/$',
|
||||||
url(r'^user/$', login_required(views.ProfileDetail.as_view()), name='profile_detail'),
|
permission_required_with_403('RIGS.add_invoice')(finance.InvoiceEvent.as_view()),
|
||||||
url(r'^user/(?P<pk>\d+)/$',
|
name='invoice_event'),
|
||||||
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
|
|
||||||
name='profile_detail'),
|
|
||||||
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
|
|
||||||
name='profile_update_self'),
|
|
||||||
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
|
|
||||||
|
|
||||||
# ICS Calendar - API key authentication
|
url(r'^invoice/(?P<pk>\d+)/$',
|
||||||
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
|
permission_required_with_403('RIGS.view_invoice')(finance.InvoiceDetail.as_view()),
|
||||||
|
name='invoice_detail'),
|
||||||
|
url(r'^invoice/(?P<pk>\d+)/print/$',
|
||||||
|
permission_required_with_403('RIGS.view_invoice')(finance.InvoicePrint.as_view()),
|
||||||
|
name='invoice_print'),
|
||||||
|
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'),
|
||||||
|
url(r'^payment/(?P<pk>\d+)/delete/$',
|
||||||
|
permission_required_with_403('RIGS.add_payment')(finance.PaymentDelete.as_view()),
|
||||||
|
name='payment_delete'),
|
||||||
|
|
||||||
# API
|
# User editing
|
||||||
url(r'^api/(?P<model>\w+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
url(r'^user/$', login_required(views.ProfileDetail.as_view()), name='profile_detail'),
|
||||||
url(r'^api/(?P<model>\w+)/(?P<pk>\d+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
url(r'^user/(?P<pk>\d+)/$',
|
||||||
|
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
|
||||||
|
name='profile_detail'),
|
||||||
|
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
|
||||||
|
name='profile_update_self'),
|
||||||
|
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
|
||||||
|
|
||||||
# Legacy URL's
|
# ICS Calendar - API key authentication
|
||||||
url(r'^rig/show/(?P<pk>\d+)/$', RedirectView.as_view(permanent=True, pattern_name='event_detail')),
|
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
|
||||||
url(r'^bookings/$', RedirectView.as_view(permanent=True, pattern_name='rigboard')),
|
|
||||||
url(r'^bookings/past/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
# API
|
||||||
]
|
url(r'^api/(?P<model>\w+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||||
|
url(r'^api/(?P<model>\w+)/(?P<pk>\d+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||||
|
|
||||||
|
# Legacy URL's
|
||||||
|
url(r'^rig/show/(?P<pk>\d+)/$', RedirectView.as_view(permanent=True, pattern_name='event_detail')),
|
||||||
|
url(r'^bookings/$', RedirectView.as_view(permanent=True, pattern_name='rigboard')),
|
||||||
|
url(r'^bookings/past/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -51,13 +41,13 @@ def model_compare(oldObj, newObj, excluded_keys=[]):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def new(self):
|
def new(self):
|
||||||
return self.display_value(self._new)
|
return self.display_value(self._new)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def long(self):
|
def long(self):
|
||||||
if isinstance(self.field, EmailField):
|
if isinstance(self.field, EmailField):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def linebreaks(self):
|
def linebreaks(self):
|
||||||
@@ -76,35 +66,43 @@ 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 = []
|
||||||
|
|
||||||
for thisField in theFields:
|
for thisField in theFields:
|
||||||
name = thisField.name
|
name = thisField.name
|
||||||
|
|
||||||
if name in excluded_keys:
|
|
||||||
continue # if we're excluding this field, skip over it
|
|
||||||
|
|
||||||
oldValue = getattr(oldObj, name, None)
|
if name in excluded_keys:
|
||||||
newValue = getattr(newObj, name, None)
|
continue # if we're excluding this field, skip over it
|
||||||
|
|
||||||
|
try:
|
||||||
|
oldValue = getattr(oldObj, name, None)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
oldValue = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
newValue = getattr(newObj, name, None)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
newValue = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
bothBlank = (not oldValue) and (not newValue)
|
bothBlank = (not oldValue) and (not newValue)
|
||||||
if oldValue != newValue and not bothBlank:
|
if oldValue != newValue and not bothBlank:
|
||||||
compare = FieldCompare(thisField,oldValue,newValue)
|
compare = FieldCompare(thisField, oldValue, newValue)
|
||||||
changes.append(compare)
|
changes.append(compare)
|
||||||
except TypeError: # logs issues with naive vs tz-aware datetimes
|
except TypeError: # logs issues with naive vs tz-aware datetimes
|
||||||
logger.error('TypeError when comparing models')
|
logger.error('TypeError when comparing models')
|
||||||
|
|
||||||
return changes
|
return changes
|
||||||
|
|
||||||
|
|
||||||
def compare_event_items(old, new):
|
def compare_event_items(old, new):
|
||||||
# Recieves two event version objects and compares their items, returns an array of ItemCompare objects
|
# Recieves two event version objects and compares their items, returns an array of ItemCompare objects
|
||||||
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
changes = []
|
item_dict[version.object_id] = compare # update the dictionary with the changes
|
||||||
|
|
||||||
|
changes = []
|
||||||
for (_, compare) in item_dict.items():
|
for (_, compare) in item_dict.items():
|
||||||
compare.changes = model_compare(compare.old, compare.new, ['id','event','order']) # see what's changed
|
compare.changes = model_compare(compare.old, compare.new, ['id', 'event', 'order']) # see what's changed
|
||||||
if len(compare.changes) >= 1:
|
if len(compare.changes) >= 1:
|
||||||
changes.append(compare) # transfer into a sequential array to make it easier to deal with later
|
changes.append(compare) # transfer into a sequential array to make it easier to deal with later
|
||||||
|
|
||||||
return changes
|
return changes
|
||||||
|
|
||||||
|
|
||||||
def get_versions_for_model(models):
|
def get_versions_for_model(models):
|
||||||
content_types = []
|
content_types = []
|
||||||
for model in models:
|
for model in models:
|
||||||
content_types.append(ContentType.objects.get_for_model(model))
|
content_types.append(ContentType.objects.get_for_model(model))
|
||||||
|
|
||||||
versions = reversion.models.Version.objects.filter(
|
versions = reversion.models.Version.objects.filter(
|
||||||
content_type__in = content_types,
|
content_type__in=content_types,
|
||||||
).select_related("revision").order_by("-pk")
|
).select_related("revision").order_by("-pk")
|
||||||
|
|
||||||
return versions
|
return versions
|
||||||
|
|
||||||
|
|
||||||
def get_previous_version(version):
|
def get_previous_version(version):
|
||||||
thisId = version.object_id
|
thisId = version.object_id
|
||||||
thisVersionId = version.pk
|
thisVersionId = version.pk
|
||||||
@@ -159,17 +161,19 @@ def get_previous_version(version):
|
|||||||
versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId)
|
versions = reversion.get_for_object_reference(version.content_type.model_class(), thisId)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
previousVersions = versions.filter(revision_id__lt=version.revision_id).latest(field_name='revision__date_created')
|
previousVersions = versions.filter(revision_id__lt=version.revision_id).latest(
|
||||||
|
field_name='revision__date_created')
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return previousVersions
|
return previousVersions
|
||||||
|
|
||||||
def get_changes_for_version(newVersion, oldVersion=None):
|
|
||||||
#Pass in a previous version if you already know it (for efficiancy)
|
|
||||||
#if not provided then it will be looked up in the database
|
|
||||||
|
|
||||||
if oldVersion == None:
|
def get_changes_for_version(newVersion, oldVersion=None):
|
||||||
|
# Pass in a previous version if you already know it (for efficiancy)
|
||||||
|
# if not provided then it will be looked up in the database
|
||||||
|
|
||||||
|
if oldVersion == None:
|
||||||
oldVersion = get_previous_version(newVersion)
|
oldVersion = get_previous_version(newVersion)
|
||||||
|
|
||||||
modelClass = newVersion.content_type.model_class()
|
modelClass = newVersion.content_type.model_class()
|
||||||
@@ -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"
|
||||||
@@ -208,7 +213,7 @@ class VersionHistory(generic.ListView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
thisModel = self.kwargs['model']
|
thisModel = self.kwargs['model']
|
||||||
|
|
||||||
context = super(VersionHistory, self).get_context_data(**kwargs)
|
context = super(VersionHistory, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
versions = context['object_list']
|
versions = context['object_list']
|
||||||
@@ -217,81 +222,82 @@ class VersionHistory(generic.ListView):
|
|||||||
items = []
|
items = []
|
||||||
|
|
||||||
for versionNo, thisVersion in enumerate(versions):
|
for versionNo, thisVersion in enumerate(versions):
|
||||||
if versionNo >= len(versions)-1:
|
if versionNo >= len(versions) - 1:
|
||||||
thisItem = get_changes_for_version(thisVersion, None)
|
thisItem = get_changes_for_version(thisVersion, None)
|
||||||
else:
|
else:
|
||||||
thisItem = get_changes_for_version(thisVersion, versions[versionNo+1])
|
thisItem = get_changes_for_version(thisVersion, versions[versionNo + 1])
|
||||||
|
|
||||||
items.append(thisItem)
|
items.append(thisItem)
|
||||||
|
|
||||||
context['object_list'] = items
|
context['object_list'] = items
|
||||||
context['object'] = thisObject
|
context['object'] = thisObject
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ActivityTable(generic.ListView):
|
class ActivityTable(generic.ListView):
|
||||||
model = reversion.revisions.Version
|
model = reversion.revisions.Version
|
||||||
template_name = "RIGS/activity_table.html"
|
template_name = "RIGS/activity_table.html"
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation])
|
versions = get_versions_for_model([models.Event, models.Venue, models.Person, models.Organisation])
|
||||||
return versions
|
return versions
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
|
|
||||||
# Call the base implementation first to get a context
|
# Call the base implementation first to get a context
|
||||||
context = super(ActivityTable, self).get_context_data(**kwargs)
|
context = super(ActivityTable, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
for thisVersion in context['object_list']:
|
for thisVersion in context['object_list']:
|
||||||
thisItem = get_changes_for_version(thisVersion, None)
|
thisItem = get_changes_for_version(thisVersion, None)
|
||||||
items.append(thisItem)
|
items.append(thisItem)
|
||||||
|
|
||||||
context ['object_list'] = items
|
context['object_list'] = items
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ActivityFeed(generic.ListView):
|
class ActivityFeed(generic.ListView):
|
||||||
model = reversion.revisions.Version
|
model = reversion.revisions.Version
|
||||||
template_name = "RIGS/activity_feed_data.html"
|
template_name = "RIGS/activity_feed_data.html"
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
versions = get_versions_for_model([models.Event,models.Venue,models.Person,models.Organisation])
|
versions = get_versions_for_model([models.Event, models.Venue, models.Person, models.Organisation])
|
||||||
return versions
|
return versions
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
maxTimeDelta = []
|
maxTimeDelta = []
|
||||||
|
|
||||||
maxTimeDelta.append({ 'maxAge':datetime.timedelta(days=1), 'group':datetime.timedelta(hours=1)})
|
maxTimeDelta.append({'maxAge': datetime.timedelta(days=1), 'group': datetime.timedelta(hours=1)})
|
||||||
maxTimeDelta.append({ 'maxAge':None, 'group':datetime.timedelta(days=1)})
|
maxTimeDelta.append({'maxAge': None, 'group': datetime.timedelta(days=1)})
|
||||||
|
|
||||||
# Call the base implementation first to get a context
|
# Call the base implementation first to get a context
|
||||||
context = super(ActivityFeed, self).get_context_data(**kwargs)
|
context = super(ActivityFeed, self).get_context_data(**kwargs)
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
for thisVersion in context['object_list']:
|
for thisVersion in context['object_list']:
|
||||||
thisItem = get_changes_for_version(thisVersion, None)
|
thisItem = get_changes_for_version(thisVersion, None)
|
||||||
if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None:
|
if thisItem['item_changes'] or thisItem['field_changes'] or thisItem['old'] == None:
|
||||||
thisItem['withPrevious'] = False
|
thisItem['withPrevious'] = False
|
||||||
if len(items)>=1:
|
if len(items) >= 1:
|
||||||
timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem['revision'].date_created
|
timeAgo = datetime.datetime.now(thisItem['revision'].date_created.tzinfo) - thisItem[
|
||||||
|
'revision'].date_created
|
||||||
timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created
|
timeDiff = items[-1]['revision'].date_created - thisItem['revision'].date_created
|
||||||
timeTogether = False
|
timeTogether = False
|
||||||
for params in maxTimeDelta:
|
for params in maxTimeDelta:
|
||||||
if params['maxAge'] is None or timeAgo <= params['maxAge']:
|
if params['maxAge'] is None or timeAgo <= params['maxAge']:
|
||||||
timeTogether = timeDiff < params['group']
|
timeTogether = timeDiff < params['group']
|
||||||
break
|
break
|
||||||
|
|
||||||
sameUser = thisItem['revision'].user == items[-1]['revision'].user
|
sameUser = thisItem['revision'].user == items[-1]['revision'].user
|
||||||
thisItem['withPrevious'] = timeTogether & sameUser
|
thisItem['withPrevious'] = timeTogether & sameUser
|
||||||
|
|
||||||
items.append(thisItem)
|
items.append(thisItem)
|
||||||
|
|
||||||
context ['object_list'] = items
|
context['object_list'] = items
|
||||||
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from django.contrib import messages
|
|||||||
import datetime, pytz
|
import datetime, pytz
|
||||||
import operator
|
import operator
|
||||||
from registration.views import RegistrationView
|
from registration.views import RegistrationView
|
||||||
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
|
||||||
|
|
||||||
from RIGS import models, forms
|
from RIGS import models, forms
|
||||||
|
|
||||||
@@ -29,12 +31,37 @@ class Index(generic.TemplateView):
|
|||||||
def login(request, **kwargs):
|
def login(request, **kwargs):
|
||||||
if request.user.is_authenticated():
|
if request.user.is_authenticated():
|
||||||
next = request.REQUEST.get('next', '/')
|
next = request.REQUEST.get('next', '/')
|
||||||
return HttpResponseRedirect(request.REQUEST.get('next', '/'))
|
return HttpResponseRedirect(next)
|
||||||
else:
|
else:
|
||||||
from django.contrib.auth.views import login
|
from django.contrib.auth.views import login
|
||||||
|
|
||||||
return login(request)
|
return login(request)
|
||||||
|
|
||||||
|
|
||||||
|
# This view should be exempt from requiring CSRF token.
|
||||||
|
# Then we can check for it and show a nice error
|
||||||
|
# Don't worry, django.contrib.auth.views.login will
|
||||||
|
# check for it before logging the user in
|
||||||
|
@csrf_exempt
|
||||||
|
def login_embed(request, **kwargs):
|
||||||
|
print("Running LOGIN")
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
next = request.REQUEST.get('next', '/')
|
||||||
|
return HttpResponseRedirect(next)
|
||||||
|
else:
|
||||||
|
from django.contrib.auth.views import login
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
csrf_cookie = request.COOKIES.get('csrftoken', None)
|
||||||
|
|
||||||
|
if csrf_cookie is None:
|
||||||
|
messages.warning(request, 'Cookies do not seem to be enabled. Try logging in using a new tab.')
|
||||||
|
request.method = 'GET' # Render the page without trying to login
|
||||||
|
|
||||||
|
return login(request, template_name="registration/login_embed.html")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
Called from a modal window (e.g. when an item is submitted to an event/invoice).
|
||||||
May optionally also include some javascript in a success message to cause a load of
|
May optionally also include some javascript in a success message to cause a load of
|
||||||
|
|||||||
53
app.json
Normal file
53
app.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
49
templates/base_embed.html
Normal file
49
templates/base_embed.html
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{% load static from staticfiles %}
|
||||||
|
{% load raven %}
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html
|
||||||
|
dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}"
|
||||||
|
xml:lang="{% firstof LANGUAGE_CODE 'en' %}"
|
||||||
|
lang="{% firstof LANGUAGE_CODE 'en' %}"
|
||||||
|
class="embedded">
|
||||||
|
<head>
|
||||||
|
<base target="_blank" />
|
||||||
|
<!-- Open all links in a new tab, not in the iframe -->
|
||||||
|
|
||||||
|
<link href='https://fonts.googleapis.com/css?family=Open+Sans:400italic,700,300,400' rel='stylesheet'
|
||||||
|
type='text/css'>
|
||||||
|
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static "css/screen.css" %}">
|
||||||
|
|
||||||
|
<script src="https://code.jquery.com/jquery-1.8.3.min.js"
|
||||||
|
integrity="sha256-YcbK69I5IXQftf/mYD8WY0/KmEDCv1asggHpJk1trM8=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.ravenjs.com/1.3.0/jquery,native/raven.min.js"></script>
|
||||||
|
<script>Raven.config('{% sentry_public_dsn %}').install()</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% include "analytics.html" %}
|
||||||
|
|
||||||
|
<div class="embed_container">
|
||||||
|
<div class="container-fluid">
|
||||||
|
{% if messages %}
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="alert alert-{{ message.level_tag }} alert-dismissible" role="alert">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span
|
||||||
|
aria-hidden="true">×</span></button>
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
templates/login_redirect.html
Normal file
24
templates/login_redirect.html
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load staticfiles %}
|
||||||
|
{% block title %}Login Required{% endblock %}
|
||||||
|
|
||||||
|
{% block js %}
|
||||||
|
<script>
|
||||||
|
document.location = "{{login_url}}"
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra-head %}
|
||||||
|
{% if oembed_url %}
|
||||||
|
<link rel="alternate" type="application/json+oembed"
|
||||||
|
href="{{oembed_url}}"
|
||||||
|
title="RIGS Embed" />
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="text-center">
|
||||||
|
<h2>Login is required for this page</h2>
|
||||||
|
<a href="{{login_url}}" class="btn btn-primary">Login</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1 +1 @@
|
|||||||
{{ user }} activation required
|
{{ user|safe }} activation required
|
||||||
|
|||||||
@@ -3,5 +3,8 @@
|
|||||||
{% block title %}Login{% endblock %}
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="text-center">
|
||||||
|
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
||||||
|
</div>
|
||||||
{% include 'registration/loginform.html' %}
|
{% include 'registration/loginform.html' %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
34
templates/registration/login_embed.html
Normal file
34
templates/registration/login_embed.html
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
{% extends 'base_embed.html' %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="text-center">
|
||||||
|
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% include 'form_errors.html' %}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4">
|
||||||
|
|
||||||
|
<form id="loginForm" action="" method="post" role="form" target="_self">{% csrf_token %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_username">{{ form.username.label }}</label>
|
||||||
|
{% render_field form.username class+="form-control" placeholder=form.username.label %}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ form.password.id_for_label }}">{{ form.password.label }}</label>
|
||||||
|
{% render_field form.password class+="form-control" placeholder=form.password.label %}
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<input type="submit" value="Login" class="btn btn-primary"/>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" name="next" value="{{ next }}"/>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
{% include 'form_errors.html' %}
|
{% include 'form_errors.html' %}
|
||||||
<div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4">
|
<div class="col-sm-6 col-sm-offset-3 col-lg-4 col-lg-offset-4">
|
||||||
|
|
||||||
<form action="{% url 'login' %}" method="post" role="form">{% csrf_token %}
|
<form action="{% url 'login' %}" method="post" role="form" target="_self">{% csrf_token %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="id_username">{{ form.username.label }}</label>
|
<label for="id_username">{{ form.username.label }}</label>
|
||||||
{% render_field form.username class+="form-control" placeholder=form.username.label autofocus="" %}
|
{% render_field form.username class+="form-control" placeholder=form.username.label autofocus="" %}
|
||||||
@@ -12,9 +12,11 @@
|
|||||||
<label for="{{ form.password.id_for_label }}">{{ form.password.label }}</label>
|
<label for="{{ form.password.id_for_label }}">{{ form.password.label }}</label>
|
||||||
{% render_field form.password class+="form-control" placeholder=form.password.label %}
|
{% render_field form.password class+="form-control" placeholder=form.password.label %}
|
||||||
</div>
|
</div>
|
||||||
<a href="{% url 'registration_register' %}" class="btn">Register</a>
|
<div class="text-right">
|
||||||
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
<a href="{% url 'registration_register' %}" class="btn">Register</a>
|
||||||
<input type="submit" value="Login" class="btn btn-primary"/>
|
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
||||||
<input type="hidden" name="next" value="{{ next }}"/>
|
<input type="submit" value="Login" class="btn btn-primary"/>
|
||||||
|
<input type="hidden" name="next" value="{{ next }}"/>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user