mirror of
https://github.com/nottinghamtec/PyRIGS.git
synced 2026-03-07 04:28:23 +00:00
Compare commits
58 Commits
hotfix/for
...
feature/di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7889d80e | ||
|
|
337dbd74fd | ||
|
|
caa55fe89a | ||
|
|
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 |
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)
|
||||
@@ -2,23 +2,37 @@ from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.shortcuts import render_to_response
|
||||
from django.template import RequestContext
|
||||
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.
|
||||
|
||||
Anonymous users will be redirected to login_url, while users that fail
|
||||
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:
|
||||
from django.conf import settings
|
||||
login_url = settings.LOGIN_URL
|
||||
|
||||
def _dec(view_func):
|
||||
def _checklogin(request, *args, **kwargs):
|
||||
if test_func(request.user):
|
||||
return view_func(request, *args, **kwargs)
|
||||
elif not request.user.is_authenticated():
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
if oembed_view is not None:
|
||||
extra_context = {}
|
||||
extra_context['oembed_url'] = "{0}://{1}{2}".format(request.scheme, request.META['HTTP_HOST'], reverse(oembed_view, kwargs=kwargs))
|
||||
extra_context['login_url'] = "{0}?{1}={2}".format(login_url, REDIRECT_FIELD_NAME, request.get_full_path())
|
||||
resp = render_to_response('login_redirect.html', extra_context, context_instance=RequestContext(request))
|
||||
return resp
|
||||
else:
|
||||
return HttpResponseRedirect('%s?%s=%s' % (login_url, REDIRECT_FIELD_NAME, request.get_full_path()))
|
||||
else:
|
||||
resp = render_to_response('403.html', context_instance=RequestContext(request))
|
||||
resp.status_code = 403
|
||||
@@ -28,14 +42,14 @@ def user_passes_test_with_403(test_func, login_url=None):
|
||||
return _checklogin
|
||||
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
|
||||
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):
|
||||
"""
|
||||
@@ -64,4 +78,4 @@ def api_key_required(function):
|
||||
if user_object.api_key != key:
|
||||
return error_resp
|
||||
return function(request, *args, **kwargs)
|
||||
return wrap
|
||||
return wrap
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -85,6 +85,7 @@ Then load the sample data using the command:
|
||||
python manage.py generateSampleData
|
||||
```
|
||||
4 user accounts are created for convenience:
|
||||
|
||||
|Username |Password |
|
||||
|---------|---------|
|
||||
|superuser|superuser|
|
||||
|
||||
@@ -122,7 +122,7 @@ class InvoiceArchive(generic.ListView):
|
||||
|
||||
class InvoiceWaiting(generic.ListView):
|
||||
model = models.Event
|
||||
# paginate_by = 25
|
||||
paginate_by = 25
|
||||
template_name = 'RIGS/event_invoice.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -9,11 +9,13 @@ from django.shortcuts import get_object_or_404
|
||||
from django.template import RequestContext
|
||||
from django.template.loader import get_template
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.http import HttpResponse
|
||||
from django.db.models import Q
|
||||
from django.contrib import messages
|
||||
from z3c.rml import rml2pdf
|
||||
from PyPDF2 import PdfFileMerger, PdfFileReader
|
||||
import simplejson
|
||||
|
||||
from RIGS import models, forms
|
||||
import datetime
|
||||
@@ -47,6 +49,28 @@ class EventDetail(generic.DetailView):
|
||||
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):
|
||||
model = models.Event
|
||||
form_class = forms.EventForm
|
||||
@@ -59,7 +83,7 @@ class EventCreate(generic.CreateView):
|
||||
form = context['form']
|
||||
if re.search('"-\d+"', form['items_json'].value()):
|
||||
messages.info(self.request, "Your item changes have been saved. Please fix the errors and save the event.")
|
||||
|
||||
|
||||
|
||||
# Get some other objects to include in the form. Used when there are errors but also nice and quick.
|
||||
for field, model in form.related_models.iteritems():
|
||||
@@ -97,11 +121,12 @@ class EventDuplicate(EventUpdate):
|
||||
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.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)
|
||||
new.pk = None # This means a new event will be created on save, and all items will be re-created
|
||||
|
||||
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
||||
else:
|
||||
messages.info(self.request, 'Event data duplicated but not yet saved. Click save to complete operation.')
|
||||
|
||||
return new
|
||||
|
||||
@@ -192,4 +217,4 @@ class EventArchive(generic.ArchiveIndexView):
|
||||
if len(qs) == 0:
|
||||
messages.add_message(self.request, messages.WARNING, "No events have been found matching those criteria.")
|
||||
|
||||
return qs
|
||||
return qs
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -147,3 +147,45 @@ ins {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
html.embedded{
|
||||
min-height:100%;
|
||||
display: table;
|
||||
width: 100%;
|
||||
|
||||
body{
|
||||
padding:0;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
width:100%;
|
||||
background:none;
|
||||
}
|
||||
|
||||
.embed_container{
|
||||
border:5px solid #e9e9e9;
|
||||
padding:12px 0px;
|
||||
min-height:100%;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.source{
|
||||
background: url('/static/imgs/pyrigs-avatar.png') no-repeat;
|
||||
background-size: 16px 16px;
|
||||
padding-left: 20px;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
h3{
|
||||
margin-top:10px;
|
||||
margin-bottom:5px;
|
||||
}
|
||||
|
||||
p{
|
||||
margin-bottom:2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.event-mic-photo{
|
||||
max-width: 3em;
|
||||
}
|
||||
}
|
||||
|
||||
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 %}
|
||||
@@ -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/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="https://form.jotformeu.com/62203600438344" target="_blank"><span class="glyphicon glyphicon-link"></span> Subhire Insurance Form</a>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,4 +77,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -438,7 +438,7 @@ class EventTest(LiveServerTestCase):
|
||||
pass
|
||||
|
||||
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(
|
||||
event=testEvent,
|
||||
@@ -471,6 +471,9 @@ class EventTest(LiveServerTestCase):
|
||||
self.assertIn("Test Item 1", 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
|
||||
form.find_element_by_xpath('//button[contains(@class, "item-add")]').click()
|
||||
wait.until(animation_is_finished())
|
||||
@@ -489,6 +492,7 @@ class EventTest(LiveServerTestCase):
|
||||
save.click()
|
||||
|
||||
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
|
||||
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
||||
@@ -498,6 +502,8 @@ class EventTest(LiveServerTestCase):
|
||||
|
||||
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)
|
||||
# Check the PO hasn't carried through
|
||||
self.assertNotIn("TESTPO", infoPanel.find_element_by_xpath('//dt[text()="PO"]/following-sibling::dd[1]').text)
|
||||
|
||||
|
||||
|
||||
@@ -506,6 +512,8 @@ class EventTest(LiveServerTestCase):
|
||||
#Check that based-on hasn't crept into the old event
|
||||
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)
|
||||
# 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
|
||||
table = self.browser.find_element_by_id('item-table') # ID number is known, see above
|
||||
|
||||
@@ -213,6 +213,88 @@ class TestInvoiceDelete(TestCase):
|
||||
# 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):
|
||||
|
||||
33
RIGS/urls.py
33
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 RIGS import models, views, rigboard, finance, ical, versioning, forms
|
||||
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 api_key_required
|
||||
@@ -14,7 +15,8 @@ urlpatterns = patterns('',
|
||||
url(r'^closemodal/$', views.CloseModal.as_view(), name='closemodal'),
|
||||
|
||||
url('^user/login/$', 'RIGS.views.login', name='login'),
|
||||
url(r'^user/password_reset/$', 'django.contrib.auth.views.password_reset', {'password_reset_form':forms.PasswordReset}),
|
||||
url('^user/login/embed/$', xframe_options_exempt(views.login_embed), name='login_embed'),
|
||||
url(r'^user/password_reset/$', 'django.contrib.auth.views.password_reset', {'password_reset_form': forms.PasswordReset}),
|
||||
|
||||
# People
|
||||
url(r'^people/$', permission_required_with_403('RIGS.view_person')(views.PersonList.as_view()),
|
||||
@@ -71,7 +73,7 @@ urlpatterns = patterns('',
|
||||
url(r'^rigboard/calendar/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
url(r'^rigboard/calendar/(?P<view>(month|week|day))/(?P<date>(\d{4}-\d{2}-\d{2}))/$', login_required()(rigboard.WebCalendar.as_view()), name='web_calendar'),
|
||||
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')),
|
||||
url(r'^rigboard/archive/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||
url(r'^rigboard/activity/$',
|
||||
permission_required_with_403('RIGS.view_event')(versioning.ActivityTable.as_view()),
|
||||
name='activity_table'),
|
||||
@@ -80,8 +82,14 @@ urlpatterns = patterns('',
|
||||
name='activity_feed'),
|
||||
|
||||
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'),
|
||||
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/$',
|
||||
permission_required_with_403('RIGS.view_event')(rigboard.EventPrint.as_view()),
|
||||
name='event_print'),
|
||||
@@ -101,7 +109,7 @@ urlpatterns = patterns('',
|
||||
permission_required_with_403('RIGS.view_event')(versioning.VersionHistory.as_view()),
|
||||
name='event_history', kwargs={'model': models.Event}),
|
||||
|
||||
|
||||
|
||||
|
||||
# Finance
|
||||
url(r'^invoice/$',
|
||||
@@ -140,10 +148,10 @@ urlpatterns = patterns('',
|
||||
# User editing
|
||||
url(r'^user/$', login_required(views.ProfileDetail.as_view()), name='profile_detail'),
|
||||
url(r'^user/(?P<pk>\d+)/$',
|
||||
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
|
||||
name='profile_detail'),
|
||||
permission_required_with_403('RIGS.view_profile')(views.ProfileDetail.as_view()),
|
||||
name='profile_detail'),
|
||||
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
|
||||
name='profile_update_self'),
|
||||
name='profile_update_self'),
|
||||
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
|
||||
|
||||
# ICS Calendar - API key authentication
|
||||
@@ -154,8 +162,7 @@ urlpatterns = patterns('',
|
||||
url(r'^api/(?P<model>\w+)/(?P<pk>\d+)/$', login_required(views.SecureAPIRequest.as_view()), name="api_secure"),
|
||||
|
||||
# Legacy URL's
|
||||
url(r'^rig/show/(?P<pk>\d+)/$', RedirectView.as_view(permanent=True,pattern_name='event_detail')),
|
||||
url(r'^bookings/$', RedirectView.as_view(permanent=True,pattern_name='rigboard')),
|
||||
url(r'^bookings/past/$', RedirectView.as_view(permanent=True,pattern_name='event_archive')),
|
||||
)
|
||||
|
||||
url(r'^rig/show/(?P<pk>\d+)/$', RedirectView.as_view(permanent=True, pattern_name='event_detail')),
|
||||
url(r'^bookings/$', RedirectView.as_view(permanent=True, pattern_name='rigboard')),
|
||||
url(r'^bookings/past/$', RedirectView.as_view(permanent=True, pattern_name='event_archive')),
|
||||
)
|
||||
|
||||
@@ -12,6 +12,8 @@ from django.contrib import messages
|
||||
import datetime, pytz
|
||||
import operator
|
||||
from registration.views import RegistrationView
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
|
||||
from RIGS import models, forms
|
||||
|
||||
@@ -29,12 +31,37 @@ class Index(generic.TemplateView):
|
||||
def login(request, **kwargs):
|
||||
if request.user.is_authenticated():
|
||||
next = request.REQUEST.get('next', '/')
|
||||
return HttpResponseRedirect(request.REQUEST.get('next', '/'))
|
||||
return HttpResponseRedirect(next)
|
||||
else:
|
||||
from django.contrib.auth.views import login
|
||||
|
||||
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).
|
||||
May optionally also include some javascript in a success message to cause a load of
|
||||
|
||||
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 %}
|
||||
@@ -3,5 +3,8 @@
|
||||
{% 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 'registration/loginform.html' %}
|
||||
{% 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' %}
|
||||
<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">
|
||||
<label for="id_username">{{ form.username.label }}</label>
|
||||
{% 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>
|
||||
{% render_field form.password class+="form-control" placeholder=form.password.label %}
|
||||
</div>
|
||||
<a href="{% url 'registration_register' %}" class="btn">Register</a>
|
||||
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
||||
<input type="submit" value="Login" class="btn btn-primary"/>
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
<div class="text-right">
|
||||
<a href="{% url 'registration_register' %}" class="btn">Register</a>
|
||||
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
|
||||
<input type="submit" value="Login" class="btn btn-primary"/>
|
||||
<input type="hidden" name="next" value="{{ next }}"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user