mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-01-17 13:32:15 +00:00
Compare commits
1 Commits
feature/in
...
feature/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7889d80e |
0
DiscourseAuth/__init__.py
Normal file
0
DiscourseAuth/__init__.py
Normal file
5
DiscourseAuth/admin.py
Normal file
5
DiscourseAuth/admin.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.contrib import admin
|
||||
from models import AuthAttempt, DiscourseUserLink
|
||||
|
||||
admin.site.register(AuthAttempt)
|
||||
admin.site.register(DiscourseUserLink)
|
||||
22
DiscourseAuth/migrations/0001_initial.py
Normal file
22
DiscourseAuth/migrations/0001_initial.py
Normal file
@@ -0,0 +1,22 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import DiscourseAuth.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='AuthAttempt',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('nonce', models.CharField(default=DiscourseAuth.models.gen_nonce, max_length=25)),
|
||||
('created', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
19
DiscourseAuth/migrations/0002_auto_20170126_1513.py
Normal file
19
DiscourseAuth/migrations/0002_auto_20170126_1513.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 = [
|
||||
('DiscourseAuth', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='authattempt',
|
||||
old_name='created',
|
||||
new_name='created_at',
|
||||
),
|
||||
]
|
||||
19
DiscourseAuth/migrations/0003_auto_20170126_1621.py
Normal file
19
DiscourseAuth/migrations/0003_auto_20170126_1621.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 = [
|
||||
('DiscourseAuth', '0002_auto_20170126_1513'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='authattempt',
|
||||
old_name='created_at',
|
||||
new_name='created',
|
||||
),
|
||||
]
|
||||
24
DiscourseAuth/migrations/0004_discourseuserlink.py
Normal file
24
DiscourseAuth/migrations/0004_discourseuserlink.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- 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),
|
||||
('DiscourseAuth', '0003_auto_20170126_1621'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DiscourseUserLink',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('discourse_user_id', models.IntegerField()),
|
||||
('django_user', models.OneToOneField(to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
19
DiscourseAuth/migrations/0005_auto_20170128_1707.py
Normal file
19
DiscourseAuth/migrations/0005_auto_20170128_1707.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 = [
|
||||
('DiscourseAuth', '0004_discourseuserlink'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='discourseuserlink',
|
||||
name='discourse_user_id',
|
||||
field=models.IntegerField(unique=True),
|
||||
),
|
||||
]
|
||||
0
DiscourseAuth/migrations/__init__.py
Normal file
0
DiscourseAuth/migrations/__init__.py
Normal file
47
DiscourseAuth/models.py
Normal file
47
DiscourseAuth/models.py
Normal file
@@ -0,0 +1,47 @@
|
||||
import uuid
|
||||
from django.db import models
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class AuthAttemptManager(models.Manager):
|
||||
expiryMinutes = 10
|
||||
|
||||
def get_acceptable(self):
|
||||
oldestAcceptableNonce = datetime.now() - timedelta(minutes=self.expiryMinutes)
|
||||
return super(AuthAttemptManager, self).get_queryset().filter(created__gte=oldestAcceptableNonce)
|
||||
|
||||
def purge_unacceptable(self):
|
||||
oldestAcceptableNonce = datetime.now() - timedelta(minutes=self.expiryMinutes)
|
||||
super(AuthAttemptManager, self).get_queryset().filter(created__lt=oldestAcceptableNonce).delete()
|
||||
|
||||
|
||||
def gen_nonce():
|
||||
# return "THISISANONCETHATWEWILLREUSE"
|
||||
return uuid.uuid4()
|
||||
|
||||
|
||||
class AuthAttempt(models.Model):
|
||||
nonce = models.CharField(max_length=25, default=gen_nonce)
|
||||
created = models.DateTimeField(auto_now=True)
|
||||
|
||||
|
||||
|
||||
objects = AuthAttemptManager()
|
||||
|
||||
def __str__(self):
|
||||
return "AuthAttempt at " + str(self.created)
|
||||
|
||||
|
||||
class DiscourseUserLink(models.Model):
|
||||
django_user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
discourse_user_id = models.IntegerField(unique=True)
|
||||
|
||||
def __str__(self):
|
||||
return "{} - {}".format(self.discourse_user_id, str(self.django_user))
|
||||
14
DiscourseAuth/templates/DiscourseAuth/associate_user.html
Normal file
14
DiscourseAuth/templates/DiscourseAuth/associate_user.html
Normal file
@@ -0,0 +1,14 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Associate User" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<p>You are currently logged in to django as "{{ djangouser }}". If this isn't you please log out</p>
|
||||
<p>If you would like to link Discourse account "{{ discourseuser }}" to your django account, click below. This will remove any existing links.</p>
|
||||
<input type="submit" value="{% trans 'Link my accounts' %}" />
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
16
DiscourseAuth/templates/DiscourseAuth/disassociate_user.html
Normal file
16
DiscourseAuth/templates/DiscourseAuth/disassociate_user.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "Associate User" %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if haslink %}
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
<p>Your account is currently linked to a Discourse account. To remove this link, click below. You will no longer be able to login using Discourse.</p>
|
||||
<input type="submit" value="{% trans 'Un-Link my accounts' %}" />
|
||||
</form>
|
||||
{% else %}
|
||||
<p>Your account is not currently linked to Discourse.</p>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
11
DiscourseAuth/urls.py
Normal file
11
DiscourseAuth/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.conf.urls import patterns, url
|
||||
|
||||
from views import StartDiscourseAuth, ContinueDiscourseAuth, NewDiscourseUser, AssociateDiscourseUser, DisassociateDiscourseUser
|
||||
|
||||
urlpatterns = patterns('DiscourseAuth',
|
||||
url(r'^start/$', StartDiscourseAuth.as_view(), name='start-auth'),
|
||||
url(r'^continue/$', ContinueDiscourseAuth.as_view(), name='continue-auth'),
|
||||
url(r'^new/$', NewDiscourseUser.as_view(), name='new-user'),
|
||||
url(r'^associate/$', AssociateDiscourseUser.as_view(), name='associate-user'),
|
||||
url(r'^disassociate/$', DisassociateDiscourseUser.as_view(), name='disassociate-user')
|
||||
)
|
||||
253
DiscourseAuth/views.py
Normal file
253
DiscourseAuth/views.py
Normal file
@@ -0,0 +1,253 @@
|
||||
|
||||
from django.views.generic import View, FormView, TemplateView
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.auth import login
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from django.core.exceptions import PermissionDenied
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
import urllib
|
||||
from hashlib import sha256
|
||||
import hmac
|
||||
from base64 import b64decode, b64encode
|
||||
from django.db.models import Q
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from django.contrib import messages
|
||||
|
||||
from models import AuthAttempt, DiscourseUserLink
|
||||
|
||||
|
||||
import time
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from registration.forms import RegistrationForm
|
||||
|
||||
|
||||
class StartDiscourseAuth(View):
|
||||
http_method_names = ['get']
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Where do we want to go once authentication is complete?
|
||||
request.session['discourse_next'] = request.GET.get('next', "/")
|
||||
|
||||
# Generate random 'nonce'
|
||||
attempt = AuthAttempt.objects.create()
|
||||
nonce = attempt.nonce
|
||||
|
||||
# Where do we want discourse to send authenticated users?
|
||||
redirect_uri = reverse('continue-auth')
|
||||
redirect_uri = request.build_absolute_uri(redirect_uri)
|
||||
|
||||
# Data to sent to Discourse (payload)
|
||||
data = {
|
||||
'nonce': nonce,
|
||||
'return_sso_url': redirect_uri
|
||||
}
|
||||
|
||||
payload = urllib.urlencode(data)
|
||||
b64payload = b64encode(payload.encode())
|
||||
|
||||
sig = hmac.new(settings.DISCOURSE_SECRET_KEY.encode(), b64payload, sha256).hexdigest()
|
||||
|
||||
return HttpResponseRedirect(settings.DISCOURSE_BASE_URL + '/session/sso_provider?' + urllib.urlencode({'sso': b64payload, 'sig': sig}))
|
||||
|
||||
|
||||
class ContinueDiscourseAuth(View):
|
||||
http_method_names = ['get']
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Where do we want to go once authentication is complete?
|
||||
nextUrl = request.session.get('discourse_next', "/")
|
||||
|
||||
rawSig = request.GET['sig']
|
||||
rawSSO = request.GET['sso']
|
||||
|
||||
payload = urllib.unquote(rawSSO)
|
||||
|
||||
computed_sig = hmac.new(
|
||||
settings.DISCOURSE_SECRET_KEY.encode(),
|
||||
payload.encode(),
|
||||
sha256
|
||||
).hexdigest()
|
||||
|
||||
successful = hmac.compare_digest(computed_sig, rawSig.encode())
|
||||
|
||||
if not successful: # The signature doesn't match, not legit
|
||||
raise ValidationError("Signature does not match, data has been manipulated")
|
||||
|
||||
decodedPayload = urllib.unquote_plus(b64decode(urllib.unquote(payload)).decode())
|
||||
data = dict(data.split("=") for data in decodedPayload.split('&'))
|
||||
|
||||
# Get the nonce that's been returned by discourse
|
||||
returnedNonce = data['nonce']
|
||||
|
||||
try: # See if it's in the database
|
||||
storedAttempt = AuthAttempt.objects.get_acceptable().get(nonce=returnedNonce)
|
||||
except AuthAttempt.DoesNotExist: # If it's not, this attempt is not valid
|
||||
raise ValidationError("Nonce does not exist in database")
|
||||
|
||||
# Delete the nonce from the database - don't allow it to be reused
|
||||
storedAttempt.delete()
|
||||
# While we're at it, delete all the other expired attempts
|
||||
AuthAttempt.objects.purge_unacceptable()
|
||||
|
||||
# If we've got this far, the attempt is valid, so let's load user information
|
||||
external_id = int(data['external_id'])
|
||||
|
||||
# See if the user is already linked to a django user
|
||||
try:
|
||||
userLink = DiscourseUserLink.objects.get(discourse_user_id=external_id)
|
||||
except DiscourseUserLink.DoesNotExist:
|
||||
return self.linkNewUser(request, data)
|
||||
|
||||
# Load the user
|
||||
user = userLink.django_user
|
||||
# Slightly hacky way to login user without calling authenticate()
|
||||
user.backend = "%s.%s" % (ModelBackend.__module__, ModelBackend.__name__)
|
||||
# Login the user
|
||||
login(request, user)
|
||||
|
||||
return HttpResponseRedirect(nextUrl)
|
||||
|
||||
def linkNewUser(self, request, data):
|
||||
# Great, let's save the new user info in the session
|
||||
request.session['discourse_data'] = data
|
||||
request.session['discourse_started_registration'] = time.time()
|
||||
|
||||
if request.user is not None:
|
||||
return HttpResponseRedirect(reverse('associate-user'))
|
||||
else:
|
||||
return HttpResponseRedirect(reverse('new-user'))
|
||||
|
||||
|
||||
class SocialRegisterForm(RegistrationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
||||
self.fields.pop('password1')
|
||||
self.fields.pop('password2')
|
||||
|
||||
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']
|
||||
|
||||
|
||||
class AssociateDiscourseUser(TemplateView):
|
||||
template_name = "DiscourseAuth/associate_user.html"
|
||||
|
||||
@method_decorator(login_required) # Require user is logged in for associating their account
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.data = self.request.session.get('discourse_data', None)
|
||||
timeStarted = self.request.session.get('discourse_started_registration', 0)
|
||||
|
||||
max_reg_time = 20 * 60 # Seconds
|
||||
|
||||
if timeStarted < (time.time() - max_reg_time):
|
||||
raise PermissionDenied('The Discourse authentication has expired, please try again')
|
||||
|
||||
if self.data is None:
|
||||
raise PermissionDenied('Discourse authentication data is not present in this session')
|
||||
|
||||
return super(AssociateDiscourseUser, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, **kwargs):
|
||||
return super(AssociateDiscourseUser, self).get(self, request, **kwargs)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
c = super(AssociateDiscourseUser, self).get_context_data()
|
||||
c['discourseuser'] = self.data['username']
|
||||
c['djangouser'] = self.request.user.username
|
||||
|
||||
return c
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
DiscourseUserLink.objects.filter(Q(django_user=request.user) | Q(discourse_user_id=self.data['external_id'])).delete()
|
||||
DiscourseUserLink.objects.create(django_user=request.user, discourse_user_id=self.data['external_id'])
|
||||
|
||||
messages.success(self.request, 'Accounts successfully linked, you are now logged in.')
|
||||
|
||||
# Redirect them to the discourse login URL
|
||||
nextUrl = "{}?next={}".format(reverse('start-auth'), request.session.get('discourse_next', "/"))
|
||||
return HttpResponseRedirect(nextUrl)
|
||||
|
||||
|
||||
class NewDiscourseUser(FormView):
|
||||
template_name = 'registration/registration_form.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.data = self.request.session.get('discourse_data', None)
|
||||
timeStarted = self.request.session.get('discourse_started_registration', 0)
|
||||
|
||||
max_reg_time = 20 * 60 # Seconds
|
||||
|
||||
if timeStarted < (time.time() - max_reg_time):
|
||||
raise PermissionDenied('The Discourse authentication has expired, please try again')
|
||||
|
||||
if self.data is None:
|
||||
raise PermissionDenied('Discourse authentication data is not present in this session')
|
||||
|
||||
return super(NewDiscourseUser, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_initial(self):
|
||||
data = self.data
|
||||
initialForm = {
|
||||
'username': data['username'],
|
||||
'email': data['email'],
|
||||
'name': data['name']
|
||||
}
|
||||
|
||||
return initialForm
|
||||
|
||||
def get_form_class(self):
|
||||
if settings.DISCOURSE_REGISTRATION_FORM:
|
||||
return settings.DISCOURSE_REGISTRATION_FORM
|
||||
else:
|
||||
return SocialRegisterForm
|
||||
|
||||
def form_valid(self, form):
|
||||
# This method is called when valid form data has been POSTed.
|
||||
user = get_user_model().objects.create_user(**form.cleaned_data)
|
||||
|
||||
# Link the user to Discourse account
|
||||
DiscourseUserLink.objects.filter(discourse_user_id=self.data['external_id']).delete()
|
||||
DiscourseUserLink.objects.create(django_user=user, discourse_user_id=self.data['external_id'])
|
||||
|
||||
messages.success(self.request, 'Account successfully created, you are now logged in.')
|
||||
|
||||
# Redirect them to the discourse login URL
|
||||
nextUrl = "{}?next={}".format(reverse('start-auth'), self.request.session.get('discourse_next', "/"))
|
||||
return HttpResponseRedirect(nextUrl)
|
||||
|
||||
|
||||
class DisassociateDiscourseUser(TemplateView):
|
||||
template_name = "DiscourseAuth/disassociate_user.html"
|
||||
|
||||
@method_decorator(login_required) # Require user is logged in for associating their account
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super(DisassociateDiscourseUser, self).dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
c = super(DisassociateDiscourseUser, self).get_context_data()
|
||||
|
||||
links = DiscourseUserLink.objects.filter(django_user=self.request.user)
|
||||
|
||||
c['haslink'] = links.count() > 0
|
||||
|
||||
return c
|
||||
|
||||
def post(self, request, **kwargs):
|
||||
DiscourseUserLink.objects.filter(django_user=request.user).delete()
|
||||
|
||||
return self.get(self, request, **kwargs)
|
||||
@@ -51,6 +51,7 @@ INSTALLED_APPS = (
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'RIGS',
|
||||
'DiscourseAuth',
|
||||
|
||||
'debug_toolbar',
|
||||
'registration',
|
||||
@@ -70,7 +71,7 @@ MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'PyRIGS.urls'
|
||||
@@ -221,3 +222,7 @@ TEMPLATE_DIRS = (
|
||||
USE_GRAVATAR=True
|
||||
|
||||
TERMS_OF_HIRE_URL = "http://www.nottinghamtec.co.uk/terms.pdf"
|
||||
|
||||
|
||||
DISCOURSE_SECRET_KEY = 'pB1f94Mfky1Y0eOrk2UjB1VqnAZ52P7v'
|
||||
DISCOURSE_BASE_URL = 'https://forum.nottinghamtec.co.uk'
|
||||
@@ -5,6 +5,7 @@ from django.conf import settings
|
||||
from registration.backends.default.views import RegistrationView
|
||||
import RIGS
|
||||
from RIGS import regbackend
|
||||
import DiscourseAuth.urls
|
||||
|
||||
urlpatterns = patterns('',
|
||||
# Examples:
|
||||
@@ -18,6 +19,7 @@ urlpatterns = patterns('',
|
||||
url('^user/', include('registration.backends.default.urls')),
|
||||
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
url(r'^discourse-auth/', include(DiscourseAuth.urls)),
|
||||
)
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import cStringIO as StringIO
|
||||
import datetime
|
||||
import re
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django.db.models import Count, Sum, F, FloatField, Q, Value
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.views import generic
|
||||
from django.db.models import Q
|
||||
from z3c.rml import rml2pdf
|
||||
|
||||
from RIGS import models
|
||||
@@ -31,8 +30,19 @@ class InvoiceIndex(generic.ListView):
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
query = self.model.objects.outstanding().select_related('event', 'event__organisation', 'event__person',
|
||||
'event__venue', 'event__mic')
|
||||
# Manual query is the only way I have found to do this efficiently. Not ideal but needs must
|
||||
sql = "SELECT * FROM " \
|
||||
"(SELECT " \
|
||||
"(SELECT COUNT(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payment_count\", " \
|
||||
"(SELECT SUM(ei.cost * ei.quantity) FROM \"RIGS_eventitem\" AS ei WHERE ei.event_id=\"RIGS_invoice\".event_id) AS \"cost\", " \
|
||||
"(SELECT SUM(p.amount) FROM \"RIGS_payment\" AS p WHERE p.invoice_id=\"RIGS_invoice\".id) AS \"payments\", " \
|
||||
"\"RIGS_invoice\".\"id\", \"RIGS_invoice\".\"event_id\", \"RIGS_invoice\".\"invoice_date\", \"RIGS_invoice\".\"void\" FROM \"RIGS_invoice\") " \
|
||||
"AS sub " \
|
||||
"WHERE (((cost > 0.0) AND (payment_count=0)) OR (cost - payments) <> 0.0) AND void = '0'" \
|
||||
"ORDER BY invoice_date"
|
||||
|
||||
query = self.model.objects.raw(sql)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
@@ -84,7 +94,6 @@ class InvoiceVoid(generic.View):
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_list'))
|
||||
return HttpResponseRedirect(reverse_lazy('invoice_detail', kwargs={'pk': object.pk}))
|
||||
|
||||
|
||||
class InvoiceDelete(generic.DeleteView):
|
||||
model = models.Invoice
|
||||
|
||||
@@ -105,7 +114,6 @@ class InvoiceDelete(generic.DeleteView):
|
||||
def get_success_url(self):
|
||||
return self.request.POST.get('next')
|
||||
|
||||
|
||||
class InvoiceArchive(generic.ListView):
|
||||
model = models.Invoice
|
||||
template_name = 'RIGS/invoice_list_archive.html'
|
||||
@@ -134,11 +142,11 @@ class InvoiceWaiting(generic.ListView):
|
||||
events = self.model.objects.filter(
|
||||
(
|
||||
Q(start_date__lte=datetime.date.today(), end_date__isnull=True) | # Starts before with no end
|
||||
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||
) & Q(invoice__isnull=True) # Has not already been invoiced
|
||||
& Q(is_rig=True) # Is a rig (not non-rig)
|
||||
|
||||
).order_by('start_date') \
|
||||
Q(end_date__lte=datetime.date.today()) # Has end date, finishes before
|
||||
) & Q(invoice__isnull=True) # Has not already been invoiced
|
||||
& Q(is_rig=True) # Is a rig (not non-rig)
|
||||
|
||||
).order_by('start_date') \
|
||||
.select_related('person',
|
||||
'organisation',
|
||||
'venue', 'mic') \
|
||||
|
||||
@@ -28,6 +28,22 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
|
||||
return self.cleaned_data['initials']
|
||||
|
||||
|
||||
class SocialRegisterForm(ProfileRegistrationFormUniqueEmail):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(SocialRegisterForm, self).__init__(*args, **kwargs)
|
||||
self.fields.pop('password1')
|
||||
self.fields.pop('password2')
|
||||
|
||||
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']
|
||||
|
||||
|
||||
# Login form
|
||||
class PasswordReset(PasswordResetForm):
|
||||
captcha = ReCaptchaField(label='Captcha')
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import pytz
|
||||
import random
|
||||
import string
|
||||
from collections import Counter
|
||||
from decimal import Decimal
|
||||
|
||||
import pytz
|
||||
import reversion
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
@@ -501,34 +501,14 @@ class EventCrew(models.Model):
|
||||
notes = models.TextField(blank=True, null=True)
|
||||
|
||||
|
||||
class InvoiceManager(models.Manager):
|
||||
def outstanding(self):
|
||||
return self.annotate(
|
||||
_payment_total=models.Sum(models.F('payment__amount'))
|
||||
).annotate(
|
||||
_sum_total=models.Sum(models.F('event__items__cost') * models.F('event__items__quantity'),
|
||||
output_field=models.DecimalField(decimal_places=2))
|
||||
# ).annotate(
|
||||
# _balance=models.ExpressionWrapper(models.F('_sum_total') - models.F('_payment_total'),
|
||||
# models.DecimalField(decimal_places=2))
|
||||
# ).filter(
|
||||
# models.Q(_balance__isnull=True) |
|
||||
# ~models.Q(_sum_total=models.F('_payment_total'))
|
||||
)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Invoice(models.Model):
|
||||
event = models.OneToOneField('Event')
|
||||
invoice_date = models.DateField(auto_now_add=True)
|
||||
void = models.BooleanField(default=False)
|
||||
|
||||
objects = InvoiceManager()
|
||||
|
||||
@property
|
||||
def sum_total(self):
|
||||
if getattr(self, '_sum_total', None):
|
||||
return Decimal(getattr(self, '_sum_total'))
|
||||
return self.event.sum_total
|
||||
|
||||
@property
|
||||
@@ -537,10 +517,6 @@ class Invoice(models.Model):
|
||||
|
||||
@property
|
||||
def payment_total(self):
|
||||
if hasattr(self, '_payment_total') and hasattr(self, '_payment_count'):
|
||||
if getattr(self, '_payment_count') == 0:
|
||||
return Decimal(0.00)
|
||||
return Decimal(getattr(self, '_payment_total', 0.00))
|
||||
total = self.payment_set.aggregate(total=models.Sum('amount'))['total']
|
||||
if total:
|
||||
return total
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
<div class="list-group-item default"></div>
|
||||
|
||||
<a class="list-group-item" href="https://forum.nottinghamtec.co.uk" target="_blank"><span class="glyphicon glyphicon-link"></span> TEC Forum</a>
|
||||
<a class="list-group-item" href="//members.nottinghamtec.co.uk/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="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>
|
||||
|
||||
Reference in New Issue
Block a user