Compare commits

...

61 Commits

Author SHA1 Message Date
David Taylor
adc94820bb Added discourse profile pictures. Will fallback to gravatar if not linked to forum account 2016-11-03 01:55:27 +00:00
David Taylor
f3947d89ca Commented registration test - since registration is now disabled 2016-11-03 00:09:09 +00:00
David Taylor
0ad3aa7d3f Fixed penguins of death due to infinite loop of SSO login redirects 2016-11-02 23:50:49 +00:00
David Taylor
01f754ad53 Removed unused views 2016-11-02 21:27:04 +00:00
David Taylor
793f1d4e05 Made it work :) 2016-11-02 21:01:40 +00:00
David Taylor
f5bf40bd9b Proof of concept discourse authentication 2016-11-02 02:43:16 +00:00
David Taylor
289b30e823 Merge pull request #271 from nottinghamtec/hotfix/duplicate-message
Do not display "not saved" message when the event has been saved. Closes #151
2016-10-23 14:15:08 +01:00
David Taylor
b939bc5a64 Do not display "not saved" message when the event has been saved 2016-10-23 14:05:25 +01:00
David Taylor
5e8f2312d3 Merge pull request #270 from nottinghamtec/po-duplicate
Remove duplicated PO numbers, closes #256
2016-10-23 13:06:29 +01:00
David Taylor
90c8b19915 Added tests for PO non-duplication 2016-10-23 12:45:27 +01:00
David Taylor
d82ef3f8d1 Merge branch 'master' into master 2016-10-23 12:36:29 +01:00
David Taylor
fc006dc53e Merge pull request #269 from johnathan99j/patch-1
Fix table display issue in README
2016-10-23 12:36:10 +01:00
Johnathan Graydon
3ce191aaf2 Update README.md
Small adjustment to make GitHub markdown display the table.
2016-10-23 12:26:35 +01:00
Johnathan Graydon
6fc89727f2 Stop PO number from duplicating when copying event
Would close #256
2016-10-21 15:48:54 +01:00
David Taylor
b235ac540f Merge pull request #265 from nottinghamtec/ra-link
Add link to pre-event RA
2016-10-19 10:35:58 +01:00
Sam Osborne
97decf8c52 Add link to pre-event RA
#accessible
2016-10-19 00:54:42 +01:00
David Taylor
97cdf34c18 Merge pull request #263 from nottinghamtec/feature/forum-embed
Forum embed
2016-10-11 18:56:02 +01:00
Tom Price
92c77c07e0 Fix tailing line breaks 2016-10-11 18:47:13 +01:00
David Taylor
0541a70cec Fixed event title link (_blank) 2016-10-09 11:30:13 +01:00
David Taylor
e0cb2f4925 Linked RIGS title 2016-10-09 11:26:32 +01:00
David Taylor
68a46af1a8 Fixed rounded corner fail 2016-10-09 11:22:34 +01:00
David Taylor
f61158b9c0 Rounded corners, transparent background 2016-10-09 11:20:43 +01:00
David Taylor
88954eca5c Removed weird background from embed 2016-10-09 10:40:18 +01:00
David Taylor
3fc04616b3 Added test for cookie warning 2016-10-09 10:36:30 +01:00
David Taylor
2d5f768523 Added cookie check with nice error message 2016-10-09 10:32:58 +01:00
David Taylor
5949ff74ec Added javascript cookie check, if blocked, login in new tab 2016-10-08 22:55:27 +01:00
David Taylor
879ecd1f6d Made font size smaller in embed 2016-10-08 21:49:03 +01:00
David Taylor
0e72c3f896 Made pretty, and made embedding accessible to non-keyholders 2016-10-08 21:38:12 +01:00
David Taylor
b93a716a3b Added unit tests 2016-10-08 20:37:01 +01:00
David Taylor
0d92c3812a Tidied up python 2016-10-08 19:56:56 +01:00
David Taylor
fc110a0bff Fixed padding 2016-10-08 19:55:31 +01:00
David Taylor
008edd8bee Lots of tidying up, moved inline CSS into SCSS 2016-10-08 19:32:45 +01:00
David Taylor
ac7e85c24a PEP8 and comments 2016-10-08 17:30:23 +01:00
David Taylor
73b8ce4add Revert "Added decorator for X-Frame header"
This reverts commit 8a838aa4bd.
2016-10-08 17:19:35 +01:00
David Taylor
511ce554b1 Revert "Try allow-from header (limited browser support)"
This reverts commit 3f4c362bfa.
2016-10-08 17:19:27 +01:00
David Taylor
536842971d Revert "Try just removing the header, this should work in all browsers"
This reverts commit 3e224a33a7.
2016-10-08 17:19:18 +01:00
David Taylor
3e224a33a7 Try just removing the header, this should work in all browsers 2016-10-08 17:14:29 +01:00
David Taylor
3f4c362bfa Try allow-from header (limited browser support) 2016-10-08 17:01:37 +01:00
David Taylor
8a838aa4bd Added decorator for X-Frame header 2016-10-07 02:51:08 +01:00
David Taylor
7e379b33db Fixed login autofocus and error messages 2016-10-07 02:24:24 +01:00
David Taylor
5e9f7e2c63 More prettying 2016-10-06 17:00:45 +01:00
David Taylor
3f752cd7b7 Made embed prettier 2016-10-06 16:48:19 +01:00
David Taylor
25a3ef3f0c Don't login in new window 2016-10-06 16:15:53 +01:00
David Taylor
1b28efb6af Allow the embedded login to be embedded (useful feature) 2016-10-06 16:10:51 +01:00
David Taylor
441a2be0b8 Added embedded login, and all iframe links open in new tab 2016-10-06 16:08:01 +01:00
David Taylor
1bdc4bd293 Fixed description = none in embed 2016-10-06 13:22:47 +01:00
David Taylor
f0bb4c5b02 Move exemption to urls.py (cleaner) 2016-10-06 13:13:09 +01:00
David Taylor
4660322964 Remove hardcoded URL 2016-10-06 13:04:33 +01:00
David Taylor
59efc2c485 Fixed JSON 2016-10-06 12:59:37 +01:00
David Taylor
69b0ff9fae Made embed page, with clickjacking protection turned off 2016-10-06 12:52:33 +01:00
David Taylor
4b94ea7ef2 Made login redirect JS for event detail 2016-10-06 12:02:44 +01:00
David Taylor
0244f5cfca Restored login security to events 2016-10-05 10:42:49 +01:00
David Taylor
17c7a3c524 Made embed tag use absolute URL 2016-10-05 10:39:50 +01:00
David Taylor
a02087bf2a Fixed fail 2016-10-04 21:11:43 +01:00
David Taylor
585f909d3f Escape JSON 2016-10-04 21:05:07 +01:00
David Taylor
eb10c8e21f Add meta to detail page 2016-10-03 23:13:25 +01:00
David Taylor
f7ea0cb834 Remove security from event detail (for testing in staging) 2016-10-03 23:09:57 +01:00
David Taylor
64f3842a13 Added iframe to embed 2016-10-03 23:02:19 +01:00
David Taylor
6370679b62 Initial proof of concept 2016-10-03 22:45:57 +01:00
David Taylor
e77728c52c Merge pull request #260 from nottinghamtec/subhire-form
Looks good, merging
2016-09-22 13:40:05 +01:00
Sam Osborne
92f4e26883 Added link to subhire form
Until such a point that subhire on RIGS actually happens, it's useful to have this link here.
2016-09-22 12:58:43 +01:00
29 changed files with 985 additions and 145 deletions

View File

@@ -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

View File

@@ -58,6 +58,7 @@ INSTALLED_APPS = (
'captcha',
'widget_tweaks',
'raven.contrib.django.raven_compat',
'social.apps.django_app.default',
)
MIDDLEWARE_CLASSES = (
@@ -73,6 +74,31 @@ MIDDLEWARE_CLASSES = (
'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'
WSGI_APPLICATION = 'PyRIGS.wsgi.application'
@@ -202,6 +228,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
"django.core.context_processors.tz",
"django.core.context_processors.request",
"django.contrib.messages.context_processors.messages",
'social.apps.django_app.context_processors.backends',
'social.apps.django_app.context_processors.login_redirect',
)

View File

@@ -18,6 +18,7 @@ urlpatterns = patterns('',
url('^user/', include('registration.backends.default.urls')),
url(r'^admin/', include(admin.site.urls)),
url('', include('social.apps.django_app.urls', namespace='social'))
)
if settings.DEBUG:

View File

@@ -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|

View File

View 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)

View 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
View 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})

View File

@@ -3,6 +3,7 @@ from django import forms
from django.utils import formats
from django.conf import settings
from django.core import serializers
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
from registration.forms import RegistrationFormUniqueEmail
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.")
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
class PasswordReset(PasswordResetForm):

View 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),
),
]

View File

@@ -22,6 +22,7 @@ class Profile(AbstractUser):
initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=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
def make_api_key(cls):
@@ -33,8 +34,13 @@ class Profile(AbstractUser):
@property
def profile_picture(self):
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:
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
return url
@property

View File

@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -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;
}
}

View 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 %}
&ndash;
{% 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 %}

View File

@@ -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 %}

View File

@@ -133,6 +133,20 @@
{% endif %}
</dd>
</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 %}
</div>

View 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 %}

View File

@@ -16,136 +16,136 @@ from selenium.webdriver.support.ui import WebDriverWait
from RIGS import models
class UserRegistrationTest(LiveServerTestCase):
# class UserRegistrationTest(LiveServerTestCase):
def setUp(self):
self.browser = webdriver.Firefox()
self.browser.implicitly_wait(3) # Set implicit wait session wide
os.environ['RECAPTCHA_TESTING'] = 'True'
# def setUp(self):
# self.browser = webdriver.Firefox()
# self.browser.implicitly_wait(3) # Set implicit wait session wide
# os.environ['RECAPTCHA_TESTING'] = 'True'
def tearDown(self):
self.browser.quit()
os.environ['RECAPTCHA_TESTING'] = 'False'
# def tearDown(self):
# self.browser.quit()
# os.environ['RECAPTCHA_TESTING'] = 'False'
def test_registration(self):
# Navigate to the registration page
self.browser.get(self.live_server_url + '/user/register/')
title_text = self.browser.find_element_by_tag_name('h3').text
self.assertIn("User Registration", title_text)
# def test_registration(self):
# # Navigate to the registration page
# self.browser.get(self.live_server_url + '/user/register/')
# title_text = self.browser.find_element_by_tag_name('h3').text
# self.assertIn("User Registration", title_text)
# Check the form invites correctly
username = self.browser.find_element_by_id('id_username')
self.assertEqual(username.get_attribute('placeholder'), 'Username')
email = self.browser.find_element_by_id('id_email')
self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
# If this is correct we don't need to test it later
self.assertEqual(email.get_attribute('type'), 'email')
password1 = self.browser.find_element_by_id('id_password1')
self.assertEqual(password1.get_attribute('placeholder'), 'Password')
self.assertEqual(password1.get_attribute('type'), 'password')
password2 = self.browser.find_element_by_id('id_password2')
self.assertEqual(
password2.get_attribute('placeholder'), 'Password confirmation')
self.assertEqual(password2.get_attribute('type'), 'password')
first_name = self.browser.find_element_by_id('id_first_name')
self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
last_name = self.browser.find_element_by_id('id_last_name')
self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
initials = self.browser.find_element_by_id('id_initials')
self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
phone = self.browser.find_element_by_id('id_phone')
self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
# # Check the form invites correctly
# username = self.browser.find_element_by_id('id_username')
# self.assertEqual(username.get_attribute('placeholder'), 'Username')
# email = self.browser.find_element_by_id('id_email')
# self.assertEqual(email.get_attribute('placeholder'), 'E-mail')
# # If this is correct we don't need to test it later
# self.assertEqual(email.get_attribute('type'), 'email')
# password1 = self.browser.find_element_by_id('id_password1')
# self.assertEqual(password1.get_attribute('placeholder'), 'Password')
# self.assertEqual(password1.get_attribute('type'), 'password')
# password2 = self.browser.find_element_by_id('id_password2')
# self.assertEqual(
# password2.get_attribute('placeholder'), 'Password confirmation')
# self.assertEqual(password2.get_attribute('type'), 'password')
# first_name = self.browser.find_element_by_id('id_first_name')
# self.assertEqual(first_name.get_attribute('placeholder'), 'First name')
# last_name = self.browser.find_element_by_id('id_last_name')
# self.assertEqual(last_name.get_attribute('placeholder'), 'Last name')
# initials = self.browser.find_element_by_id('id_initials')
# self.assertEqual(initials.get_attribute('placeholder'), 'Initials')
# phone = self.browser.find_element_by_id('id_phone')
# self.assertEqual(phone.get_attribute('placeholder'), 'Phone')
# Fill the form out incorrectly
username.send_keys('TestUsername')
email.send_keys('test@example.com')
password1.send_keys('correcthorsebatterystaple')
# deliberate mistake
password2.send_keys('correcthorsebatterystapleerror')
first_name.send_keys('John')
last_name.send_keys('Smith')
initials.send_keys('JS')
phone.send_keys('0123456789')
self.browser.execute_script(
"return jQuery('#g-recaptcha-response').val('PASSED')")
# # Fill the form out incorrectly
# username.send_keys('TestUsername')
# email.send_keys('test@example.com')
# password1.send_keys('correcthorsebatterystaple')
# # deliberate mistake
# password2.send_keys('correcthorsebatterystapleerror')
# first_name.send_keys('John')
# last_name.send_keys('Smith')
# initials.send_keys('JS')
# phone.send_keys('0123456789')
# self.browser.execute_script(
# "return jQuery('#g-recaptcha-response').val('PASSED')")
# Submit incorrect form
submit = self.browser.find_element_by_xpath("//input[@type='submit']")
submit.click()
# # Submit incorrect form
# submit = self.browser.find_element_by_xpath("//input[@type='submit']")
# submit.click()
# Restablish error fields
password1 = self.browser.find_element_by_id('id_password1')
password2 = self.browser.find_element_by_id('id_password2')
# # Restablish error fields
# password1 = self.browser.find_element_by_id('id_password1')
# password2 = self.browser.find_element_by_id('id_password2')
# Read what the error is
alert = self.browser.find_element_by_css_selector(
'div.alert-danger').text
self.assertIn("password fields didn't match", alert)
# # Read what the error is
# alert = self.browser.find_element_by_css_selector(
# 'div.alert-danger').text
# self.assertIn("password fields didn't match", alert)
# Passwords should be empty
self.assertEqual(password1.get_attribute('value'), '')
self.assertEqual(password2.get_attribute('value'), '')
# # Passwords should be empty
# self.assertEqual(password1.get_attribute('value'), '')
# self.assertEqual(password2.get_attribute('value'), '')
# Correct error
password1.send_keys('correcthorsebatterystaple')
password2.send_keys('correcthorsebatterystaple')
self.browser.execute_script(
"return jQuery('#g-recaptcha-response').val('PASSED')")
# # Correct error
# password1.send_keys('correcthorsebatterystaple')
# password2.send_keys('correcthorsebatterystaple')
# self.browser.execute_script(
# "return jQuery('#g-recaptcha-response').val('PASSED')")
# Submit again
password2.send_keys(Keys.ENTER)
# # Submit again
# password2.send_keys(Keys.ENTER)
# Check we have a success message
alert = self.browser.find_element_by_css_selector(
'div.alert-success').text
self.assertIn('register', alert)
self.assertIn('email', alert)
# # Check we have a success message
# alert = self.browser.find_element_by_css_selector(
# 'div.alert-success').text
# self.assertIn('register', alert)
# self.assertIn('email', alert)
# Check Email
self.assertEqual(len(mail.outbox), 1)
email = mail.outbox[0]
self.assertIn('John Smith "JS" activation required', email.subject)
urls = re.findall(
'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
self.assertEqual(len(urls), 1)
# # Check Email
# self.assertEqual(len(mail.outbox), 1)
# email = mail.outbox[0]
# self.assertIn('John Smith "JS" activation required', email.subject)
# urls = re.findall(
# 'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', email.body)
# self.assertEqual(len(urls), 1)
mail.outbox = [] # empty this for later
# mail.outbox = [] # empty this for later
# Follow link
self.browser.get(urls[0]) # go to the first link
# # Follow link
# self.browser.get(urls[0]) # go to the first link
# Complete registration
title_text = self.browser.find_element_by_tag_name('h2').text
self.assertIn('Complete', title_text)
# # Complete registration
# title_text = self.browser.find_element_by_tag_name('h2').text
# self.assertIn('Complete', title_text)
# Test login
self.browser.get(self.live_server_url + '/user/login')
username = self.browser.find_element_by_id('id_username')
self.assertEqual(username.get_attribute('placeholder'), 'Username')
password = self.browser.find_element_by_id('id_password')
self.assertEqual(password.get_attribute('placeholder'), 'Password')
self.assertEqual(password.get_attribute('type'), 'password')
# # Test login
# self.browser.get(self.live_server_url + '/user/login')
# username = self.browser.find_element_by_id('id_username')
# self.assertEqual(username.get_attribute('placeholder'), 'Username')
# password = self.browser.find_element_by_id('id_password')
# self.assertEqual(password.get_attribute('placeholder'), 'Password')
# self.assertEqual(password.get_attribute('type'), 'password')
username.send_keys('TestUsername')
password.send_keys('correcthorsebatterystaple')
self.browser.execute_script(
"return jQuery('#g-recaptcha-response').val('PASSED')")
password.send_keys(Keys.ENTER)
# username.send_keys('TestUsername')
# password.send_keys('correcthorsebatterystaple')
# self.browser.execute_script(
# "return jQuery('#g-recaptcha-response').val('PASSED')")
# password.send_keys(Keys.ENTER)
# Check we are logged in
udd = self.browser.find_element_by_class_name('navbar').text
self.assertIn('Hi John', udd)
# # Check we are logged in
# udd = self.browser.find_element_by_class_name('navbar').text
# self.assertIn('Hi John', udd)
# Check all the data actually got saved
profileObject = models.Profile.objects.all()[0]
self.assertEqual(profileObject.username, 'TestUsername')
self.assertEqual(profileObject.first_name, 'John')
self.assertEqual(profileObject.last_name, 'Smith')
self.assertEqual(profileObject.initials, 'JS')
self.assertEqual(profileObject.phone, '0123456789')
self.assertEqual(profileObject.email, 'test@example.com')
# # Check all the data actually got saved
# profileObject = models.Profile.objects.all()[0]
# self.assertEqual(profileObject.username, 'TestUsername')
# self.assertEqual(profileObject.first_name, 'John')
# self.assertEqual(profileObject.last_name, 'Smith')
# self.assertEqual(profileObject.initials, 'JS')
# self.assertEqual(profileObject.phone, '0123456789')
# self.assertEqual(profileObject.email, 'test@example.com')
# All is well
# # All is well
class EventTest(LiveServerTestCase):
@@ -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

View File

@@ -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):

View File

@@ -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,11 +148,12 @@ 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'),
url(r'^user/unlink_forum$', login_required(views.UnlinkForum.as_view(permanent=False)), name='unlink_forum'),
# 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"),
@@ -154,8 +163,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')),
)

View File

@@ -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
@@ -355,3 +382,12 @@ class ResetApiKey(generic.RedirectView):
self.request.user.save()
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')

View File

@@ -12,17 +12,23 @@ django-widget-tweaks==1.3
gunicorn==19.3.0
icalendar==3.9.0
lxml==3.4.4
oauthlib==2.0.0
Pillow==2.8.1
psycopg2==2.6
Pygments==2.0.2
PyJWT==1.4.2
PyPDF2==1.24
python-dateutil==2.4.2
python-openid==2.2.5
python-social-auth==0.2.21
pytz==2015.4
raven==5.8.1
reportlab==3.1.44
requests==2.11.1
requests-oauthlib==0.7.0
selenium==2.53.6
simplejson==3.7.2
six==1.9.0
six==1.10.0
sqlparse==0.1.15
static3==0.6.1
svg2rlg==0.3

49
templates/base_embed.html Normal file
View 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">&times;</span></button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
</div>
</div>
{% block js %}
{% endblock %}
</body>
</html>

View 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 %}

View File

@@ -1,7 +1,49 @@
{% extends 'base.html' %}
{% load static from staticfiles %}
{% block title %}Login{% endblock %}
{% block content %}
{% include 'registration/loginform.html' %}
<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' %}
</div>
</div>
</div>
</div>
{% endblock %}

View 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 %}

View File

@@ -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>