mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-07 12:38:23 +00:00
Compare commits
91 Commits
feature/su
...
feature/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adc94820bb | ||
|
|
f3947d89ca | ||
|
|
0ad3aa7d3f | ||
|
|
01f754ad53 | ||
|
|
793f1d4e05 | ||
|
|
f5bf40bd9b | ||
|
|
289b30e823 | ||
|
|
b939bc5a64 | ||
|
|
5e8f2312d3 | ||
|
|
90c8b19915 | ||
|
|
d82ef3f8d1 | ||
|
|
fc006dc53e | ||
|
|
3ce191aaf2 | ||
|
|
6fc89727f2 | ||
|
|
b235ac540f | ||
|
|
97decf8c52 | ||
|
|
97cdf34c18 | ||
|
|
92c77c07e0 | ||
|
|
0541a70cec | ||
|
|
e0cb2f4925 | ||
|
|
68a46af1a8 | ||
|
|
f61158b9c0 | ||
|
|
88954eca5c | ||
|
|
3fc04616b3 | ||
|
|
2d5f768523 | ||
|
|
5949ff74ec | ||
|
|
879ecd1f6d | ||
|
|
0e72c3f896 | ||
|
|
b93a716a3b | ||
|
|
0d92c3812a | ||
|
|
fc110a0bff | ||
|
|
008edd8bee | ||
|
|
ac7e85c24a | ||
|
|
73b8ce4add | ||
|
|
511ce554b1 | ||
|
|
536842971d | ||
|
|
3e224a33a7 | ||
|
|
3f4c362bfa | ||
|
|
8a838aa4bd | ||
|
|
7e379b33db | ||
|
|
5e9f7e2c63 | ||
|
|
3f752cd7b7 | ||
|
|
25a3ef3f0c | ||
|
|
1b28efb6af | ||
|
|
441a2be0b8 | ||
|
|
1bdc4bd293 | ||
|
|
f0bb4c5b02 | ||
|
|
4660322964 | ||
|
|
59efc2c485 | ||
|
|
69b0ff9fae | ||
|
|
4b94ea7ef2 | ||
|
|
0244f5cfca | ||
|
|
17c7a3c524 | ||
|
|
a02087bf2a | ||
|
|
585f909d3f | ||
|
|
eb10c8e21f | ||
|
|
f7ea0cb834 | ||
|
|
64f3842a13 | ||
|
|
6370679b62 | ||
|
|
e77728c52c | ||
|
|
92f4e26883 | ||
|
|
024f08562b | ||
|
|
b4246fe170 | ||
|
|
fc6db5bff2 | ||
|
|
c5d3e7c0f2 | ||
|
|
10b57adb37 | ||
|
|
4f839d05f9 | ||
|
|
84393e9e4a | ||
|
|
b94cef92d2 | ||
|
|
10c04be051 | ||
|
|
7ecd6212ac | ||
|
|
11180e507c | ||
|
|
1c90ce5b41 | ||
|
|
11dd9ad02f | ||
|
|
27c0deaba3 | ||
|
|
4c09258566 | ||
|
|
7d14429d84 | ||
|
|
190825f5ef | ||
|
|
021edfd39d | ||
|
|
3052f28329 | ||
|
|
4cec20e357 | ||
|
|
8a0cbe32bd | ||
|
|
8a3a52a21b | ||
|
|
98a9b22e0e | ||
|
|
9178cf6062 | ||
|
|
abbb20e49e | ||
|
|
01d2eae7bc | ||
|
|
39d27d2730 | ||
|
|
05b2de561e | ||
|
|
667b0c80ca | ||
|
|
67624eea6f |
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,22 +2,36 @@ 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():
|
||||||
|
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()))
|
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))
|
||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,10 +21,15 @@ SECRET_KEY = os.environ.get('SECRET_KEY') if os.environ.get('SECRET_KEY') else '
|
|||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
|
DEBUG = bool(int(os.environ.get('DEBUG'))) if os.environ.get('DEBUG') else True
|
||||||
|
|
||||||
|
STAGING = bool(int(os.environ.get('STAGING'))) if os.environ.get('STAGING') else False
|
||||||
|
|
||||||
TEMPLATE_DEBUG = True
|
TEMPLATE_DEBUG = True
|
||||||
|
|
||||||
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
ALLOWED_HOSTS = ['pyrigs.nottinghamtec.co.uk', 'rigs.nottinghamtec.co.uk', 'pyrigs.herokuapp.com']
|
||||||
|
|
||||||
|
if STAGING:
|
||||||
|
ALLOWED_HOSTS.append('.herokuapp.com')
|
||||||
|
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
|
SECURE_SSL_REDIRECT = True # Redirect all http requests to https
|
||||||
@@ -46,7 +51,6 @@ INSTALLED_APPS = (
|
|||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'RIGS',
|
'RIGS',
|
||||||
'subhire',
|
|
||||||
|
|
||||||
'debug_toolbar',
|
'debug_toolbar',
|
||||||
'registration',
|
'registration',
|
||||||
@@ -54,6 +58,7 @@ INSTALLED_APPS = (
|
|||||||
'captcha',
|
'captcha',
|
||||||
'widget_tweaks',
|
'widget_tweaks',
|
||||||
'raven.contrib.django.raven_compat',
|
'raven.contrib.django.raven_compat',
|
||||||
|
'social.apps.django_app.default',
|
||||||
)
|
)
|
||||||
|
|
||||||
MIDDLEWARE_CLASSES = (
|
MIDDLEWARE_CLASSES = (
|
||||||
@@ -69,6 +74,31 @@ MIDDLEWARE_CLASSES = (
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = (
|
||||||
|
'RIGS.discourse.discourse.DiscourseAuth',
|
||||||
|
'django.contrib.auth.backends.ModelBackend',
|
||||||
|
)
|
||||||
|
|
||||||
|
SOCIAL_AUTH_PIPELINE = (
|
||||||
|
'social.pipeline.social_auth.social_details', # Load remote details
|
||||||
|
'social.pipeline.social_auth.social_uid', # Load remote ID
|
||||||
|
'social.pipeline.social_auth.auth_allowed', # Check not blacklisted
|
||||||
|
'social.pipeline.social_auth.social_user', # If already associated, login
|
||||||
|
'RIGS.discourse.pipeline.new_connection', # Choose a user account, much UI
|
||||||
|
'social.pipeline.social_auth.associate_user', # Associate the social auth with the user
|
||||||
|
'social.pipeline.social_auth.load_extra_data', # Save all the social info we have on this user
|
||||||
|
'RIGS.discourse.pipeline.update_avatar', # Load the avatar URL from the API, and save to user model
|
||||||
|
'social.pipeline.user.user_details', # Save any details that changed
|
||||||
|
)
|
||||||
|
|
||||||
|
DISCOURSE_HOST = os.environ.get('DISCOURSE_HOST') if os.environ.get('DISCOURSE_HOST') else 'http://localhost:4000'
|
||||||
|
DISCOURSE_SSO_SECRET = os.environ.get('DISCOURSE_SSO_SECRET') if os.environ.get('DISCOURSE_SSO_SECRET') else 'ABCDEFGHIJKLMNOP'
|
||||||
|
|
||||||
|
DISCOURSE_API_KEY = os.environ.get('DISCOURSE_API_KEY') if os.environ.get('DISCOURSE_HOST') else None
|
||||||
|
DISCOURSE_API_USER = os.environ.get('DISCOURSE_API_USER') if os.environ.get('DISCOURSE_HOST') else 'system'
|
||||||
|
|
||||||
|
REGISTRATION_OPEN = False # Disable built-in django registration - must register using forum
|
||||||
|
|
||||||
ROOT_URLCONF = 'PyRIGS.urls'
|
ROOT_URLCONF = 'PyRIGS.urls'
|
||||||
|
|
||||||
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
|
||||||
@@ -198,6 +228,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
|
|||||||
"django.core.context_processors.tz",
|
"django.core.context_processors.tz",
|
||||||
"django.core.context_processors.request",
|
"django.core.context_processors.request",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
'social.apps.django_app.context_processors.backends',
|
||||||
|
'social.apps.django_app.context_processors.login_redirect',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ urlpatterns = patterns('',
|
|||||||
url('^user/', include('registration.backends.default.urls')),
|
url('^user/', include('registration.backends.default.urls')),
|
||||||
|
|
||||||
url(r'^admin/', include(admin.site.urls)),
|
url(r'^admin/', include(admin.site.urls)),
|
||||||
|
url('', include('social.apps.django_app.urls', namespace='social'))
|
||||||
)
|
)
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
|||||||
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.
|
||||||
|
|||||||
91
RIGS/discourse/discourse.py
Normal file
91
RIGS/discourse/discourse.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
from social.backends.base import BaseAuth
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from .sso import DiscourseSSO
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseAssociation(object):
|
||||||
|
""" Use Association model to save the nonce by force. """
|
||||||
|
|
||||||
|
def __init__(self, handle, secret='', issued=0, lifetime=0, assoc_type=''):
|
||||||
|
self.handle = handle # as nonce
|
||||||
|
self.secret = secret.encode() # not use
|
||||||
|
self.issued = issued # not use
|
||||||
|
self.lifetime = lifetime # not use
|
||||||
|
self.assoc_type = assoc_type # as state
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseAuth(BaseAuth):
|
||||||
|
"""Discourse authentication backend"""
|
||||||
|
name = 'discourse'
|
||||||
|
secret = settings.DISCOURSE_SSO_SECRET
|
||||||
|
host = settings.DISCOURSE_HOST
|
||||||
|
|
||||||
|
EXTRA_DATA = [
|
||||||
|
('username', 'username'),
|
||||||
|
('email', 'email'),
|
||||||
|
('external_id', 'external_id')
|
||||||
|
]
|
||||||
|
|
||||||
|
sso = DiscourseSSO(secret)
|
||||||
|
|
||||||
|
def get_and_store_nonce(self, url):
|
||||||
|
# Create a nonce
|
||||||
|
nonce = self.strategy.random_string(64)
|
||||||
|
# Store the nonce
|
||||||
|
association = DiscourseAssociation(nonce)
|
||||||
|
self.strategy.storage.association.store(url, association)
|
||||||
|
return nonce
|
||||||
|
|
||||||
|
def get_nonce(self, nonce):
|
||||||
|
try:
|
||||||
|
return self.strategy.storage.association.get(
|
||||||
|
server_url=self.host,
|
||||||
|
handle=nonce
|
||||||
|
)[0]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def remove_nonce(self, nonce_id):
|
||||||
|
self.strategy.storage.association.remove([nonce_id])
|
||||||
|
|
||||||
|
def get_user_id(self, details, response):
|
||||||
|
"""Return current user id."""
|
||||||
|
|
||||||
|
return int(response['external_id'])
|
||||||
|
|
||||||
|
def get_user_details(self, response):
|
||||||
|
"""Return user basic information (id and email only)."""
|
||||||
|
|
||||||
|
return {'username': response['username'],
|
||||||
|
'email': response['email'],
|
||||||
|
'fullname': response['name'].replace('+', ' ') if 'name' in response else '',
|
||||||
|
'first_name': '',
|
||||||
|
'last_name': ''}
|
||||||
|
|
||||||
|
def auth_url(self):
|
||||||
|
"""Build and return complete URL."""
|
||||||
|
nonce = self.get_and_store_nonce(self.host)
|
||||||
|
|
||||||
|
return self.host + self.sso.build_login_URL(nonce, self.redirect_uri)
|
||||||
|
|
||||||
|
def auth_complete(self, *args, **kwargs):
|
||||||
|
"""Completes login process, must return user instance."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not self.sso.validate(self.data['sso'], self.data['sig']):
|
||||||
|
raise Exception("Someone wants to hack us!")
|
||||||
|
except KeyError:
|
||||||
|
raise Exception("SSO Error, please try again")
|
||||||
|
|
||||||
|
nonce = self.sso.get_nonce(self.data['sso'])
|
||||||
|
nonce_obj = self.get_nonce(nonce)
|
||||||
|
if nonce_obj:
|
||||||
|
self.remove_nonce(nonce_obj.id)
|
||||||
|
else:
|
||||||
|
raise Exception("Nonce does not match!")
|
||||||
|
|
||||||
|
kwargs.update({'response': self.sso.get_data(
|
||||||
|
self.data['sso']), 'backend': self})
|
||||||
|
return self.strategy.authenticate(*args, **kwargs)
|
||||||
88
RIGS/discourse/pipeline.py
Normal file
88
RIGS/discourse/pipeline.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||||
|
from django.shortcuts import render_to_response
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from social.pipeline.partial import partial
|
||||||
|
|
||||||
|
from RIGS.models import Profile
|
||||||
|
from RIGS import forms
|
||||||
|
|
||||||
|
|
||||||
|
class SocialRegisterForm(forms.ProfileRegistrationFormUniqueEmail):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields.pop('password1')
|
||||||
|
self.fields.pop('password2')
|
||||||
|
self.fields.pop('captcha')
|
||||||
|
|
||||||
|
self.fields['email'].widget.attrs['readonly'] = True
|
||||||
|
|
||||||
|
def clean_email(self):
|
||||||
|
initial = getattr(self, 'initial', None)
|
||||||
|
if(initial['email'] != self.cleaned_data['email']):
|
||||||
|
raise ValidationError("You cannot change the email")
|
||||||
|
|
||||||
|
return initial['email']
|
||||||
|
|
||||||
|
|
||||||
|
@partial
|
||||||
|
def new_connection(backend, details, response, user=None, is_new=False, social=None, request=None, *args, **kwargs):
|
||||||
|
if social is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
data = backend.strategy.request_data()
|
||||||
|
|
||||||
|
if data.get('UseCurrentAccount') is not None:
|
||||||
|
return
|
||||||
|
|
||||||
|
alreadyLoggedIn = user is not None
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'details': details,
|
||||||
|
'alreadyLoggedIn': alreadyLoggedIn,
|
||||||
|
'loggedInUser': user,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not alreadyLoggedIn:
|
||||||
|
completeUrl = reverse('social:complete', kwargs={'backend': backend.name})
|
||||||
|
context['login_url'] = "{0}?{1}={2}".format(reverse('login'), REDIRECT_FIELD_NAME, completeUrl)
|
||||||
|
|
||||||
|
if data.get('username') is None:
|
||||||
|
form = SocialRegisterForm(initial=details)
|
||||||
|
else:
|
||||||
|
form = SocialRegisterForm(data, initial=details)
|
||||||
|
|
||||||
|
if form.is_valid():
|
||||||
|
new_user = Profile.objects.create_user(**form.cleaned_data)
|
||||||
|
return {'user': new_user}
|
||||||
|
|
||||||
|
context['form'] = form
|
||||||
|
|
||||||
|
return render_to_response('RIGS/social-associate.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
def update_avatar(backend, details, response, user=None, social=None, *args, **kwargs):
|
||||||
|
host = settings.DISCOURSE_HOST
|
||||||
|
api_key = settings.DISCOURSE_API_KEY
|
||||||
|
api_user = settings.DISCOURSE_API_USER
|
||||||
|
if social is not None:
|
||||||
|
url = "{}/users/{}.json".format(host, details['username'])
|
||||||
|
params = {
|
||||||
|
'api_key': api_key,
|
||||||
|
'api_username': api_user
|
||||||
|
}
|
||||||
|
resp = requests.get(url=url, params=params)
|
||||||
|
extraData = json.loads(resp.text)
|
||||||
|
|
||||||
|
avatar_template = extraData['user']['avatar_template']
|
||||||
|
|
||||||
|
if avatar_template and user.avatar_template != avatar_template:
|
||||||
|
user.avatar_template = avatar_template
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
return
|
||||||
46
RIGS/discourse/sso.py
Normal file
46
RIGS/discourse/sso.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import urllib
|
||||||
|
from hashlib import sha256
|
||||||
|
import hmac
|
||||||
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
|
|
||||||
|
class DiscourseSSO:
|
||||||
|
def __init__(self, secret_key):
|
||||||
|
self.__secret_key = secret_key
|
||||||
|
|
||||||
|
def validate(self, payload, sig):
|
||||||
|
payload = urllib.unquote(payload)
|
||||||
|
computed_sig = hmac.new(
|
||||||
|
self.__secret_key.encode(),
|
||||||
|
payload.encode(),
|
||||||
|
sha256
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
return hmac.compare_digest(unicode(computed_sig), sig)
|
||||||
|
|
||||||
|
def get_nonce(self, payload):
|
||||||
|
payload = b64decode(urllib.unquote(payload)).decode()
|
||||||
|
d = dict(nonce.split("=") for nonce in payload.split('&'))
|
||||||
|
|
||||||
|
if 'nonce' in d and d['nonce'] != '':
|
||||||
|
return d['nonce']
|
||||||
|
else:
|
||||||
|
raise Exception("Nonce could not be found in payload")
|
||||||
|
|
||||||
|
def get_data(self, payload):
|
||||||
|
payload = urllib.unquote(b64decode(urllib.unquote(payload)).decode())
|
||||||
|
d = dict(data.split("=") for data in payload.split('&'))
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
def build_login_URL(self, nonce, redirect_uri):
|
||||||
|
data = {
|
||||||
|
'nonce': nonce,
|
||||||
|
'return_sso_url': redirect_uri
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = urllib.urlencode(data)
|
||||||
|
payload = b64encode(payload.encode())
|
||||||
|
sig = hmac.new(self.__secret_key.encode(), payload, sha256).hexdigest()
|
||||||
|
|
||||||
|
return '/session/sso_provider?' + urllib.urlencode({'sso': payload, 'sig': sig})
|
||||||
@@ -94,6 +94,25 @@ 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
|
||||||
@@ -103,7 +122,7 @@ class InvoiceArchive(generic.ListView):
|
|||||||
|
|
||||||
class InvoiceWaiting(generic.ListView):
|
class InvoiceWaiting(generic.ListView):
|
||||||
model = models.Event
|
model = models.Event
|
||||||
paginate_by = 25
|
# paginate_by = 25
|
||||||
template_name = 'RIGS/event_invoice.html'
|
template_name = 'RIGS/event_invoice.html'
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@@ -144,6 +163,7 @@ 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}))
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from django import forms
|
|||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
|
||||||
from registration.forms import RegistrationFormUniqueEmail
|
from registration.forms import RegistrationFormUniqueEmail
|
||||||
from captcha.fields import ReCaptchaField
|
from captcha.fields import ReCaptchaField
|
||||||
@@ -27,6 +28,16 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
|||||||
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
raise forms.ValidationError("These initials are already in use. Please supply different initials.")
|
||||||
return self.cleaned_data['initials']
|
return self.cleaned_data['initials']
|
||||||
|
|
||||||
|
def clean_first_name(self):
|
||||||
|
if self.cleaned_data["first_name"].strip() == '':
|
||||||
|
raise ValidationError("First name is required.")
|
||||||
|
return self.cleaned_data["first_name"]
|
||||||
|
|
||||||
|
def clean_last_name(self):
|
||||||
|
if self.cleaned_data["last_name"].strip() == '':
|
||||||
|
raise ValidationError("Last name is required.")
|
||||||
|
return self.cleaned_data["last_name"]
|
||||||
|
|
||||||
|
|
||||||
# Login form
|
# Login form
|
||||||
class PasswordReset(PasswordResetForm):
|
class PasswordReset(PasswordResetForm):
|
||||||
|
|||||||
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())
|
||||||
19
RIGS/migrations/0025_profile_avatar_template.py
Normal file
19
RIGS/migrations/0025_profile_avatar_template.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import models, migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('RIGS', '0024_auto_20160229_2042'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='profile',
|
||||||
|
name='avatar_template',
|
||||||
|
field=models.CharField(max_length=255, null=True, editable=False, blank=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -22,6 +22,7 @@ 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)
|
||||||
|
avatar_template = models.CharField(max_length=255, blank=True, editable=False, null=True)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_api_key(cls):
|
def make_api_key(cls):
|
||||||
@@ -33,8 +34,13 @@ class Profile(AbstractUser):
|
|||||||
@property
|
@property
|
||||||
def profile_picture(self):
|
def profile_picture(self):
|
||||||
url = ""
|
url = ""
|
||||||
|
if settings.DISCOURSE_API_KEY is not None:
|
||||||
|
if self.avatar_template:
|
||||||
|
return settings.DISCOURSE_HOST+self.avatar_template.format(size=500)
|
||||||
|
|
||||||
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
|
||||||
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
|
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
|
||||||
|
|
||||||
return url
|
return url
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -526,6 +532,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,28 @@ 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',
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -97,10 +121,11 @@ 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
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
BIN
RIGS/static/imgs/forum-logo.gif
Normal file
BIN
RIGS/static/imgs/forum-logo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 300 KiB |
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 %}
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
} else {
|
} else {
|
||||||
$('.form-is_rig').slideDown();
|
$('.form-is_rig').slideDown();
|
||||||
}
|
}
|
||||||
$('.form-hws').css('overflow', 'visible');
|
$('.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();
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>Event #</th>
|
||||||
<th>Start Date</th>
|
<th>Start Date</th>
|
||||||
<th>Event Name</th>
|
<th>Event Name</th>
|
||||||
<th>Client</th>
|
<th>Client</th>
|
||||||
|
|||||||
@@ -25,7 +25,9 @@
|
|||||||
|
|
||||||
<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="//members.nottinghamtec.co.uk/forum" 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>
|
||||||
|
|||||||
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>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>#</th>
|
<th>Invoice #</th>
|
||||||
<th>Event</th>
|
<th>Event</th>
|
||||||
<th>Client</th>
|
<th>Client</th>
|
||||||
<th>Event Date</th>
|
<th>Event Date</th>
|
||||||
@@ -27,10 +27,25 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for object in object_list %}
|
{% for object in object_list %}
|
||||||
<tr class="{% if object.void %}danger{% elif object.balance == 0 %}success{% endif %}">
|
<tr>
|
||||||
<td>{{ object.pk }}</td>
|
<td class="{% if object.is_closed %}success{% else %}warning{% endif %}">{{ object.pk }}<br>
|
||||||
<td><a href="{% url 'event_detail' object.event.pk %}">N{{ object.event.pk|stringformat:"05d" }}</a>: {{ object.event.name }} <br>
|
<span class="text-muted">{% if object.void %}(VOID){% elif object.is_closed %}(PAID){% else %}(O/S){% endif %}</span></td>
|
||||||
<span class="text-muted">{{ object.event.get_status_display }}</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>
|
||||||
<td>{% if object.event.organisation %}
|
<td>{% if object.event.organisation %}
|
||||||
{{ object.event.organisation.name }}
|
{{ object.event.organisation.name }}
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -133,6 +133,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
<h4>Linked to {{object.social_auth.count}} Forum Account(s)</h4>
|
||||||
|
|
||||||
|
{% if object.social_auth.count > 0 %}
|
||||||
|
|
||||||
|
<a href="{% url 'unlink_forum' %}" class="btn btn-danger">
|
||||||
|
Unlink Forum Account(s) <span class="glyphicon glyphicon-pencil"></span>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% url "social:begin" "discourse" %}" class="btn btn-success">
|
||||||
|
Link Forum Account <span class="glyphicon glyphicon-pencil"></span>
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
55
RIGS/templates/RIGS/social-associate.html
Normal file
55
RIGS/templates/RIGS/social-associate.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
{% block title %}Associate{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="col-sm-10 col-sm-offset-1">
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-center">Welcome <strong>{{details.username}}</strong></h2>
|
||||||
|
<h4 class="text-center">This is the first time you've visited RIGS with your forum account, so we need a few details to get you set up</h4>
|
||||||
|
<hr/>
|
||||||
|
|
||||||
|
|
||||||
|
{% if alreadyLoggedIn %}
|
||||||
|
<h2 class="text-center">You are logged in to RIGS as <strong>{{loggedInUser.username}}</strong></h2>
|
||||||
|
<div class="col-sm-8 col-sm-offset-2">
|
||||||
|
<form action="", method="post">{% csrf_token %}
|
||||||
|
<input type="hidden" name="UseCurrentAccount" value="1"/>
|
||||||
|
<button type="submit" class="btn btn-lg btn-primary center btn-block">Link Forum account to RIGS Account</button>
|
||||||
|
</form>
|
||||||
|
<a class="btn btn-lg btn-warning center btn-block" href="{% url 'logout' %}" role="button">Logout</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h4 class="text-center"><a class="btn btn-info" href="{{login_url}}" role="button">I already have a RIGS account</a></h4>
|
||||||
|
{% if form.errors or supplement_form.errors %}
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
{{form.errors}}
|
||||||
|
{{supplement_form.errors}}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="col-sm-8 col-sm-offset-2">
|
||||||
|
<form action="" method="post" class="form-horizontal" role="form">{% csrf_token %}
|
||||||
|
{% for field in form %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="{{ field.id_for_label }}" class="control-label col-sm-4">{{ field.label }}</label>
|
||||||
|
<div class="controls col-sm-8">
|
||||||
|
{% render_field field class+="form-control" placeholder=field.label %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<p><input type="submit" value="Register" class="btn btn-primary pull-right"></p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% 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>
|
<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">
|
||||||
|
|||||||
@@ -1,150 +1,151 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import date, timedelta
|
||||||
|
|
||||||
|
import reversion
|
||||||
|
from django.core import mail
|
||||||
|
from django.db import transaction
|
||||||
from django.test import LiveServerTestCase
|
from django.test import LiveServerTestCase
|
||||||
from django.test.client import Client
|
from django.test.client import Client
|
||||||
from django.core import mail
|
|
||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.webdriver.common.keys import Keys
|
|
||||||
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
|
from selenium.common.exceptions import StaleElementReferenceException, WebDriverException
|
||||||
|
from selenium.webdriver.common.keys import Keys
|
||||||
from selenium.webdriver.support.ui import WebDriverWait
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
|
||||||
from RIGS import models
|
from RIGS import models
|
||||||
import re
|
|
||||||
import os
|
|
||||||
from datetime import date, timedelta
|
|
||||||
from django.db import transaction
|
|
||||||
import reversion
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
class UserRegistrationTest(LiveServerTestCase):
|
# class UserRegistrationTest(LiveServerTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
# def setUp(self):
|
||||||
self.browser = webdriver.Firefox()
|
# self.browser = webdriver.Firefox()
|
||||||
self.browser.implicitly_wait(3) # Set implicit wait session wide
|
# self.browser.implicitly_wait(3) # Set implicit wait session wide
|
||||||
os.environ['RECAPTCHA_TESTING'] = 'True'
|
# os.environ['RECAPTCHA_TESTING'] = 'True'
|
||||||
|
|
||||||
def tearDown(self):
|
# def tearDown(self):
|
||||||
self.browser.quit()
|
# self.browser.quit()
|
||||||
os.environ['RECAPTCHA_TESTING'] = 'False'
|
# os.environ['RECAPTCHA_TESTING'] = 'False'
|
||||||
|
|
||||||
def test_registration(self):
|
# def test_registration(self):
|
||||||
# Navigate to the registration page
|
# # Navigate to the registration page
|
||||||
self.browser.get(self.live_server_url + '/user/register/')
|
# self.browser.get(self.live_server_url + '/user/register/')
|
||||||
title_text = self.browser.find_element_by_tag_name('h3').text
|
# title_text = self.browser.find_element_by_tag_name('h3').text
|
||||||
self.assertIn("User Registration", title_text)
|
# self.assertIn("User Registration", title_text)
|
||||||
|
|
||||||
# Check the form invites correctly
|
# # Check the form invites correctly
|
||||||
username = self.browser.find_element_by_id('id_username')
|
# username = self.browser.find_element_by_id('id_username')
|
||||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
# self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||||
email = self.browser.find_element_by_id('id_email')
|
# email = self.browser.find_element_by_id('id_email')
|
||||||
self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
|
# self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
|
||||||
# If this is correct we don't need to test it later
|
# # If this is correct we don't need to test it later
|
||||||
self.assertEqual(email.get_attribute('type'), 'email')
|
# self.assertEqual(email.get_attribute('type'), 'email')
|
||||||
password1 = self.browser.find_element_by_id('id_password1')
|
# password1 = self.browser.find_element_by_id('id_password1')
|
||||||
self.assertEqual(password1.get_attribute('placeholder'), 'Password')
|
# self.assertEqual(password1.get_attribute('placeholder'), 'Password')
|
||||||
self.assertEqual(password1.get_attribute('type'), 'password')
|
# self.assertEqual(password1.get_attribute('type'), 'password')
|
||||||
password2 = self.browser.find_element_by_id('id_password2')
|
# password2 = self.browser.find_element_by_id('id_password2')
|
||||||
self.assertEqual(
|
# self.assertEqual(
|
||||||
password2.get_attribute('placeholder'), 'Password confirmation')
|
# password2.get_attribute('placeholder'), 'Password confirmation')
|
||||||
self.assertEqual(password2.get_attribute('type'), 'password')
|
# self.assertEqual(password2.get_attribute('type'), 'password')
|
||||||
first_name = self.browser.find_element_by_id('id_first_name')
|
# first_name = self.browser.find_element_by_id('id_first_name')
|
||||||
self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
|
# self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
|
||||||
last_name = self.browser.find_element_by_id('id_last_name')
|
# last_name = self.browser.find_element_by_id('id_last_name')
|
||||||
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
# self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
|
||||||
initials = self.browser.find_element_by_id('id_initials')
|
# initials = self.browser.find_element_by_id('id_initials')
|
||||||
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
# self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
|
||||||
phone = self.browser.find_element_by_id('id_phone')
|
# phone = self.browser.find_element_by_id('id_phone')
|
||||||
self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
# self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
|
||||||
|
|
||||||
# Fill the form out incorrectly
|
# # Fill the form out incorrectly
|
||||||
username.send_keys('TestUsername')
|
# username.send_keys('TestUsername')
|
||||||
email.send_keys('test@example.com')
|
# email.send_keys('test@example.com')
|
||||||
password1.send_keys('correcthorsebatterystaple')
|
# password1.send_keys('correcthorsebatterystaple')
|
||||||
# deliberate mistake
|
# # deliberate mistake
|
||||||
password2.send_keys('correcthorsebatterystapleerror')
|
# password2.send_keys('correcthorsebatterystapleerror')
|
||||||
first_name.send_keys('John')
|
# first_name.send_keys('John')
|
||||||
last_name.send_keys('Smith')
|
# last_name.send_keys('Smith')
|
||||||
initials.send_keys('JS')
|
# initials.send_keys('JS')
|
||||||
phone.send_keys('0123456789')
|
# phone.send_keys('0123456789')
|
||||||
self.browser.execute_script(
|
# self.browser.execute_script(
|
||||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
# "return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||||
|
|
||||||
# Submit incorrect form
|
# # Submit incorrect form
|
||||||
submit = self.browser.find_element_by_xpath("//input[@type='submit']")
|
# submit = self.browser.find_element_by_xpath("//input[@type='submit']")
|
||||||
submit.click()
|
# submit.click()
|
||||||
|
|
||||||
# Restablish error fields
|
# # Restablish error fields
|
||||||
password1 = self.browser.find_element_by_id('id_password1')
|
# password1 = self.browser.find_element_by_id('id_password1')
|
||||||
password2 = self.browser.find_element_by_id('id_password2')
|
# password2 = self.browser.find_element_by_id('id_password2')
|
||||||
|
|
||||||
# Read what the error is
|
# # Read what the error is
|
||||||
alert = self.browser.find_element_by_css_selector(
|
# alert = self.browser.find_element_by_css_selector(
|
||||||
'div.alert-danger').text
|
# 'div.alert-danger').text
|
||||||
self.assertIn("password fields didn't match", alert)
|
# self.assertIn("password fields didn't match", alert)
|
||||||
|
|
||||||
# Passwords should be empty
|
# # Passwords should be empty
|
||||||
self.assertEqual(password1.get_attribute('value'), '')
|
# self.assertEqual(password1.get_attribute('value'), '')
|
||||||
self.assertEqual(password2.get_attribute('value'), '')
|
# self.assertEqual(password2.get_attribute('value'), '')
|
||||||
|
|
||||||
# Correct error
|
# # Correct error
|
||||||
password1.send_keys('correcthorsebatterystaple')
|
# password1.send_keys('correcthorsebatterystaple')
|
||||||
password2.send_keys('correcthorsebatterystaple')
|
# password2.send_keys('correcthorsebatterystaple')
|
||||||
self.browser.execute_script(
|
# self.browser.execute_script(
|
||||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
# "return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||||
|
|
||||||
# Submit again
|
# # Submit again
|
||||||
password2.send_keys(Keys.ENTER)
|
# password2.send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Check we have a success message
|
# # Check we have a success message
|
||||||
alert = self.browser.find_element_by_css_selector(
|
# alert = self.browser.find_element_by_css_selector(
|
||||||
'div.alert-success').text
|
# 'div.alert-success').text
|
||||||
self.assertIn('register', alert)
|
# self.assertIn('register', alert)
|
||||||
self.assertIn('email', alert)
|
# self.assertIn('email', alert)
|
||||||
|
|
||||||
# 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)
|
||||||
|
|
||||||
mail.outbox = [] # empty this for later
|
# mail.outbox = [] # empty this for later
|
||||||
|
|
||||||
# Follow link
|
# # Follow link
|
||||||
self.browser.get(urls[0]) # go to the first link
|
# self.browser.get(urls[0]) # go to the first link
|
||||||
|
|
||||||
# Complete registration
|
# # Complete registration
|
||||||
title_text = self.browser.find_element_by_tag_name('h2').text
|
# title_text = self.browser.find_element_by_tag_name('h2').text
|
||||||
self.assertIn('Complete', title_text)
|
# self.assertIn('Complete', title_text)
|
||||||
|
|
||||||
# Test login
|
# # Test login
|
||||||
self.browser.get(self.live_server_url + '/user/login')
|
# self.browser.get(self.live_server_url + '/user/login')
|
||||||
username = self.browser.find_element_by_id('id_username')
|
# username = self.browser.find_element_by_id('id_username')
|
||||||
self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
# self.assertEqual(username.get_attribute('placeholder'), 'Username')
|
||||||
password = self.browser.find_element_by_id('id_password')
|
# password = self.browser.find_element_by_id('id_password')
|
||||||
self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
# self.assertEqual(password.get_attribute('placeholder'), 'Password')
|
||||||
self.assertEqual(password.get_attribute('type'), 'password')
|
# self.assertEqual(password.get_attribute('type'), 'password')
|
||||||
|
|
||||||
username.send_keys('TestUsername')
|
# username.send_keys('TestUsername')
|
||||||
password.send_keys('correcthorsebatterystaple')
|
# password.send_keys('correcthorsebatterystaple')
|
||||||
self.browser.execute_script(
|
# self.browser.execute_script(
|
||||||
"return jQuery('#g-recaptcha-response').val('PASSED')")
|
# "return jQuery('#g-recaptcha-response').val('PASSED')")
|
||||||
password.send_keys(Keys.ENTER)
|
# password.send_keys(Keys.ENTER)
|
||||||
|
|
||||||
# Check we are logged in
|
# # Check we are logged in
|
||||||
udd = self.browser.find_element_by_class_name('navbar').text
|
# udd = self.browser.find_element_by_class_name('navbar').text
|
||||||
self.assertIn('Hi John', udd)
|
# self.assertIn('Hi John', udd)
|
||||||
|
|
||||||
# Check all the data actually got saved
|
# # Check all the data actually got saved
|
||||||
profileObject = models.Profile.objects.all()[0]
|
# profileObject = models.Profile.objects.all()[0]
|
||||||
self.assertEqual(profileObject.username, 'TestUsername')
|
# self.assertEqual(profileObject.username, 'TestUsername')
|
||||||
self.assertEqual(profileObject.first_name, 'John')
|
# self.assertEqual(profileObject.first_name, 'John')
|
||||||
self.assertEqual(profileObject.last_name, 'Smith')
|
# self.assertEqual(profileObject.last_name, 'Smith')
|
||||||
self.assertEqual(profileObject.initials, 'JS')
|
# self.assertEqual(profileObject.initials, 'JS')
|
||||||
self.assertEqual(profileObject.phone, '0123456789')
|
# self.assertEqual(profileObject.phone, '0123456789')
|
||||||
self.assertEqual(profileObject.email, 'test@example.com')
|
# self.assertEqual(profileObject.email, 'test@example.com')
|
||||||
|
|
||||||
# All is well
|
# # All is well
|
||||||
|
|
||||||
|
|
||||||
class EventTest(LiveServerTestCase):
|
class EventTest(LiveServerTestCase):
|
||||||
@@ -437,7 +438,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 +471,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())
|
||||||
@@ -488,6 +492,7 @@ class EventTest(LiveServerTestCase):
|
|||||||
save.click()
|
save.click()
|
||||||
|
|
||||||
self.assertNotIn("N0000%d"%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
|
||||||
|
|
||||||
# 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
|
||||||
@@ -497,6 +502,8 @@ class EventTest(LiveServerTestCase):
|
|||||||
|
|
||||||
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
infoPanel = self.browser.find_element_by_xpath('//div[contains(text(), "Event Info")]/..')
|
||||||
self.assertIn("N0000%d"%testEvent.pk, infoPanel.find_element_by_xpath('//dt[text()="Based On"]/following-sibling::dd[1]').text)
|
self.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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -505,6 +512,8 @@ 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")]/..')
|
||||||
self.assertNotIn("N0000%d"%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)
|
||||||
|
|
||||||
# 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
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
from django.core.urlresolvers import reverse
|
|
||||||
from django.test import TestCase
|
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from RIGS import models
|
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
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):
|
class TestAdminMergeObjects(TestCase):
|
||||||
@@ -155,3 +158,153 @@ class TestAdminMergeObjects(TestCase):
|
|||||||
if event.organisation == self.organisations[3]: # The one we left in place
|
if event.organisation == self.organisations[3]: # The one we left in place
|
||||||
continue
|
continue
|
||||||
self.assertEqual(updatedEvent.organisation, self.organisations[1])
|
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')
|
||||||
|
|||||||
17
RIGS/urls.py
17
RIGS/urls.py
@@ -1,7 +1,8 @@
|
|||||||
from django.conf.urls import patterns, include, url
|
from django.conf.urls import patterns, url
|
||||||
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
|
||||||
@@ -14,6 +15,7 @@ urlpatterns = patterns('',
|
|||||||
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
||||||
|
|
||||||
url('^user/login/$', 'RIGS.views.login', name='login'),
|
url('^user/login/$', 'RIGS.views.login', name='login'),
|
||||||
|
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
|
||||||
url(r'^user/password_reset/$', 'django.contrib.auth.views.password_reset', {'password_reset_form': forms.PasswordReset}),
|
url(r'^user/password_reset/$', 'django.contrib.auth.views.password_reset', {'password_reset_form': forms.PasswordReset}),
|
||||||
|
|
||||||
# People
|
# People
|
||||||
@@ -80,8 +82,14 @@ urlpatterns = patterns('',
|
|||||||
name='activity_feed'),
|
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+)/embed/$',
|
||||||
|
xframe_options_exempt(login_required(login_url='/user/login/embed/')(rigboard.EventEmbed.as_view())),
|
||||||
|
name='event_embed'),
|
||||||
|
url(r'^event/(?P<pk>\d+)/oembed_json/$',
|
||||||
|
rigboard.EventOembed.as_view(),
|
||||||
|
name='event_oembed'),
|
||||||
url(r'^event/(?P<pk>\d+)/print/$',
|
url(r'^event/(?P<pk>\d+)/print/$',
|
||||||
permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
|
permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
|
||||||
name='event_print'),
|
name='event_print'),
|
||||||
@@ -127,6 +135,9 @@ urlpatterns = patterns('',
|
|||||||
url(r'^invoice/(?P<pk>\d+)/void/$',
|
url(r'^invoice/(?P<pk>\d+)/void/$',
|
||||||
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()),
|
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceVoid.as_view()),
|
||||||
name='invoice_void'),
|
name='invoice_void'),
|
||||||
|
url(r'^invoice/(?P<pk>\d+)/delete/$',
|
||||||
|
permission_required_with_403('RIGS.change_invoice')(finance.InvoiceDelete.as_view()),
|
||||||
|
name='invoice_delete'),
|
||||||
url(r'^payment/create/$',
|
url(r'^payment/create/$',
|
||||||
permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
|
permission_required_with_403('RIGS.add_payment')(finance.PaymentCreate.as_view()),
|
||||||
name='payment_create'),
|
name='payment_create'),
|
||||||
@@ -142,6 +153,7 @@ urlpatterns = patterns('',
|
|||||||
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
|
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
|
||||||
name='profile_update_self'),
|
name='profile_update_self'),
|
||||||
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
|
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
|
||||||
|
url(r'^user/unlink_forum$', login_required(views.UnlinkForum.as_view(permanent=False)), name='unlink_forum'),
|
||||||
|
|
||||||
# ICS Calendar - API key authentication
|
# ICS Calendar - API key authentication
|
||||||
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
|
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),
|
||||||
@@ -155,4 +167,3 @@ urlpatterns = patterns('',
|
|||||||
url(r'^bookings/$', RedirectView.as_view(permanent=True, pattern_name='rigboard')),
|
url(r'^bookings/$', RedirectView.as_view(permanent=True, pattern_name='rigboard')),
|
||||||
url(r'^bookings/past/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
url(r'^bookings/past/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -355,3 +382,12 @@ class ResetApiKey(generic.RedirectView):
|
|||||||
self.request.user.save()
|
self.request.user.save()
|
||||||
|
|
||||||
return reverse_lazy('profile_detail')
|
return reverse_lazy('profile_detail')
|
||||||
|
|
||||||
|
class UnlinkForum(generic.RedirectView):
|
||||||
|
def get_redirect_url(self, *args, **kwargs):
|
||||||
|
for link in self.request.user.social_auth.all():
|
||||||
|
link.delete()
|
||||||
|
|
||||||
|
self.request.user.save()
|
||||||
|
|
||||||
|
return reverse_lazy('profile_detail')
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -12,17 +12,23 @@ django-widget-tweaks==1.3
|
|||||||
gunicorn==19.3.0
|
gunicorn==19.3.0
|
||||||
icalendar==3.9.0
|
icalendar==3.9.0
|
||||||
lxml==3.4.4
|
lxml==3.4.4
|
||||||
|
oauthlib==2.0.0
|
||||||
Pillow==2.8.1
|
Pillow==2.8.1
|
||||||
psycopg2==2.6
|
psycopg2==2.6
|
||||||
Pygments==2.0.2
|
Pygments==2.0.2
|
||||||
|
PyJWT==1.4.2
|
||||||
PyPDF2==1.24
|
PyPDF2==1.24
|
||||||
python-dateutil==2.4.2
|
python-dateutil==2.4.2
|
||||||
|
python-openid==2.2.5
|
||||||
|
python-social-auth==0.2.21
|
||||||
pytz==2015.4
|
pytz==2015.4
|
||||||
raven==5.8.1
|
raven==5.8.1
|
||||||
reportlab==3.1.44
|
reportlab==3.1.44
|
||||||
selenium==2.53.1
|
requests==2.11.1
|
||||||
|
requests-oauthlib==0.7.0
|
||||||
|
selenium==2.53.6
|
||||||
simplejson==3.7.2
|
simplejson==3.7.2
|
||||||
six==1.9.0
|
six==1.10.0
|
||||||
sqlparse==0.1.15
|
sqlparse==0.1.15
|
||||||
static3==0.6.1
|
static3==0.6.1
|
||||||
svg2rlg==0.3
|
svg2rlg==0.3
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
import models
|
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
admin.site.register(models.Hire)
|
|
||||||
admin.site.register(models.Provider)
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
from django.db import models, migrations
|
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Hire',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('description', models.TextField(null=True, blank=True)),
|
|
||||||
('start_date', models.DateField()),
|
|
||||||
('end_date', models.DateField()),
|
|
||||||
('start_transport', models.IntegerField(blank=True, null=True, choices=[(0, b'TEC Transport'), (1, b'Provider Transports')])),
|
|
||||||
('mic', models.ForeignKey(related_name='hire_mic', verbose_name=b'MIC', blank=True, to=settings.AUTH_USER_MODEL, null=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Provider',
|
|
||||||
fields=[
|
|
||||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
|
||||||
('name', models.CharField(max_length=50)),
|
|
||||||
('phone', models.CharField(max_length=15, null=True, blank=True)),
|
|
||||||
('email', models.EmailField(max_length=254, null=True, blank=True)),
|
|
||||||
('address', models.TextField(null=True, blank=True)),
|
|
||||||
('notes', models.TextField(null=True, blank=True)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='hire',
|
|
||||||
name='provider',
|
|
||||||
field=models.ForeignKey(blank=True, to='subhire.Provider', null=True),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import reversion
|
|
||||||
from django.db import models
|
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.encoding import python_2_unicode_compatible
|
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
class Hire(models.Model):
|
|
||||||
WE_TRANSPORT = 0
|
|
||||||
THEY_TRANSPORT = 1
|
|
||||||
TRANSPORT_CHOICES = (
|
|
||||||
(WE_TRANSPORT, 'TEC Transport'),
|
|
||||||
(THEY_TRANSPORT, 'Provider Transports'),
|
|
||||||
)
|
|
||||||
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
description = models.TextField(blank=True, null=True)
|
|
||||||
provider = models.ForeignKey('Provider', blank=True, null=True)
|
|
||||||
|
|
||||||
start_date = models.DateField()
|
|
||||||
end_date = models.DateField()
|
|
||||||
|
|
||||||
start_transport = models.IntegerField(
|
|
||||||
choices=TRANSPORT_CHOICES, blank=True, null=True)
|
|
||||||
|
|
||||||
mic = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='hire_mic', blank=True, null=True,
|
|
||||||
verbose_name="MIC")
|
|
||||||
|
|
||||||
class Provider(models.Model):
|
|
||||||
name = models.CharField(max_length=50)
|
|
||||||
phone = models.CharField(max_length=15, blank=True, null=True)
|
|
||||||
email = models.EmailField(blank=True, null=True)
|
|
||||||
|
|
||||||
address = models.TextField(blank=True, null=True)
|
|
||||||
|
|
||||||
notes = models.TextField(blank=True, null=True)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def latest_hires(self):
|
|
||||||
return self.hire_set.order_by('-start_date').select_related()
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
@@ -76,9 +76,9 @@
|
|||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
{% 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"></span> Outstanding</a>
|
<li><a href="{% url 'invoice_list' %}"><span class="glyphicon glyphicon-gbp text-warning"></span> Outstanding</a>
|
||||||
</li>
|
</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>
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -1,7 +1,49 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
{% load static from staticfiles %}
|
||||||
|
|
||||||
{% 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>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="panel-group">
|
||||||
|
{% url "social:complete" "discourse" as completeUrl %}
|
||||||
|
{% if not request.GET.next == completeUrl %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
|
||||||
|
Login with TEC Forum
|
||||||
|
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div id="forumLogin">
|
||||||
|
<div class="panel-body" style="text-align:center;">
|
||||||
|
<a class="btn btn-default" href="{% url "social:begin" "discourse" %}?next={{request.GET.next}}">
|
||||||
|
|
||||||
|
<h4>Login using</h4>
|
||||||
|
<img src="{% static "imgs/forum-logo.gif" %}" width=200></img>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
Login with RIGS Credentials
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="panel-body">
|
||||||
{% include 'registration/loginform.html' %}
|
{% include 'registration/loginform.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% 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 'registration_register' %}" class="btn">Register</a> #}
|
||||||
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
||||||
<input type="submit" value="Login" class="btn btn-primary"/>
|
<input type="submit" value="Login" class="btn btn-primary"/>
|
||||||
<input type="hidden" name="next" value="{{ next }}"/>
|
<input type="hidden" name="next" value="{{ next }}"/>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
103
wercker.yml
103
wercker.yml
@@ -1,103 +0,0 @@
|
|||||||
# This references the default Python container from
|
|
||||||
# the Docker Hub with the 2.7 tag:
|
|
||||||
# https://registry.hub.docker.com/_/python/
|
|
||||||
# If you want to use a slim Python container with
|
|
||||||
# version 3.4.3 you would use: python:3.4-slim
|
|
||||||
# If you want Google's container you would reference google/python
|
|
||||||
# Read more about containers on our dev center
|
|
||||||
# http://devcenter.wercker.com/docs/containers/index.html
|
|
||||||
box: heroku/python
|
|
||||||
# You can also use services such as databases. Read more on our dev center:
|
|
||||||
# http://devcenter.wercker.com/docs/services/index.html
|
|
||||||
services:
|
|
||||||
# - id: postgres
|
|
||||||
# env:
|
|
||||||
# POSTGRES_PASSWORD: pyrigstesting
|
|
||||||
# POSTGRES_USER: pyrigs
|
|
||||||
# http://devcenter.wercker.com/docs/services/postgresql.html
|
|
||||||
|
|
||||||
# - mongodb
|
|
||||||
# http://devcenter.wercker.com/docs/services/mongodb.html
|
|
||||||
|
|
||||||
# This is the build pipeline. Pipelines are the core of wercker
|
|
||||||
# Read more about pipelines on our dev center
|
|
||||||
# http://devcenter.wercker.com/docs/pipelines/index.html
|
|
||||||
build:
|
|
||||||
# The steps that will be executed on build
|
|
||||||
# Steps make up the actions in your pipeline
|
|
||||||
# Read more about steps on our dev center:
|
|
||||||
# http://devcenter.wercker.com/docs/steps/index.html
|
|
||||||
steps:
|
|
||||||
- install-packages:
|
|
||||||
packages: firefox xvfb
|
|
||||||
|
|
||||||
- script:
|
|
||||||
name: Enable virtual display
|
|
||||||
code: |-
|
|
||||||
# Start xvfb which gives the context an virtual display
|
|
||||||
# which is required for tests that require an GUI
|
|
||||||
export DISPLAY=:99.0
|
|
||||||
start-stop-daemon --start --quiet --pidfile /tmp/xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1024x768x24 -ac +extension GLX +render -noreset
|
|
||||||
# Give xvfb time to start. 3 seconds is the default for all xvfb-run commands.
|
|
||||||
# sleep 3
|
|
||||||
|
|
||||||
- script:
|
|
||||||
name: virtualenv install
|
|
||||||
code: |
|
|
||||||
pip install virtualenv
|
|
||||||
|
|
||||||
# A step that sets up the python virtual environment
|
|
||||||
- virtualenv:
|
|
||||||
name: setup virtual environment
|
|
||||||
install_wheel: false # Enable wheel to speed up builds (experimental)
|
|
||||||
|
|
||||||
# # Use this virtualenv step for python 3.2
|
|
||||||
# - virtualenv
|
|
||||||
# name: setup virtual environment
|
|
||||||
# python_location: /usr/bin/python3.2
|
|
||||||
|
|
||||||
# # This pip-install clears the local wheel cache
|
|
||||||
# - pip-install:
|
|
||||||
# clean_wheel_dir: true
|
|
||||||
|
|
||||||
# A custom script step, name value is used in the UI
|
|
||||||
# and the code value contains the command that get executed
|
|
||||||
- script:
|
|
||||||
name: Python Version
|
|
||||||
code: |
|
|
||||||
echo "python version $(python --version) running"
|
|
||||||
echo "pip version $(pip --version) running"
|
|
||||||
|
|
||||||
# Django uses this to connect to the database
|
|
||||||
# - script:
|
|
||||||
# name: set environment
|
|
||||||
# code: |
|
|
||||||
# export DEBUG=0
|
|
||||||
# export DATABASE_URL="postgres://pyrigs:pyrigstesting@$POSTGRES_PORT_5432_TCP_ADDR:$POSTGRES_PORT_5432_TCP_PORT$POSTGRES_NAME"
|
|
||||||
|
|
||||||
# A step that executes `pip install` command.
|
|
||||||
- pip-install
|
|
||||||
|
|
||||||
# Install coverage
|
|
||||||
- script:
|
|
||||||
name: install coverage
|
|
||||||
code: |
|
|
||||||
pip install coverage
|
|
||||||
|
|
||||||
# Collect static files so the manifest storage knows where to look
|
|
||||||
- script:
|
|
||||||
name: collect static
|
|
||||||
code: |
|
|
||||||
python manage.py collectstatic --noinput
|
|
||||||
|
|
||||||
# Run python tests
|
|
||||||
- script:
|
|
||||||
name: run tests
|
|
||||||
code: |
|
|
||||||
coverage run manage.py test RIGS
|
|
||||||
|
|
||||||
- script:
|
|
||||||
name: collect coverage data
|
|
||||||
code: |
|
|
||||||
coverage report -m --include=PyRIGS/*.*,RIGS/*.* --omit=*/migrations/* | tee coverage.txt
|
|
||||||
coverage html --include=PyRIGS/*.*,RIGS/*.* --omit=*/migrations/*
|
|
||||||
Reference in New Issue
Block a user