Compare commits

...

6 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
18 changed files with 526 additions and 112 deletions

View File

@@ -58,6 +58,7 @@ INSTALLED_APPS = (
'captcha', 'captcha',
'widget_tweaks', 'widget_tweaks',
'raven.contrib.django.raven_compat', 'raven.contrib.django.raven_compat',
'social.apps.django_app.default',
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
@@ -73,6 +74,31 @@ MIDDLEWARE_CLASSES = (
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
) )
AUTHENTICATION_BACKENDS = (
'RIGS.discourse.discourse.DiscourseAuth',
'django.contrib.auth.backends.ModelBackend',
)
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details', # Load remote details
'social.pipeline.social_auth.social_uid', # Load remote ID
'social.pipeline.social_auth.auth_allowed', # Check not blacklisted
'social.pipeline.social_auth.social_user', # If already associated, login
'RIGS.discourse.pipeline.new_connection', # Choose a user account, much UI
'social.pipeline.social_auth.associate_user', # Associate the social auth with the user
'social.pipeline.social_auth.load_extra_data', # Save all the social info we have on this user
'RIGS.discourse.pipeline.update_avatar', # Load the avatar URL from the API, and save to user model
'social.pipeline.user.user_details', # Save any details that changed
)
DISCOURSE_HOST = os.environ.get('DISCOURSE_HOST') if os.environ.get('DISCOURSE_HOST') else 'http://localhost:4000'
DISCOURSE_SSO_SECRET = os.environ.get('DISCOURSE_SSO_SECRET') if os.environ.get('DISCOURSE_SSO_SECRET') else 'ABCDEFGHIJKLMNOP'
DISCOURSE_API_KEY = os.environ.get('DISCOURSE_API_KEY') if os.environ.get('DISCOURSE_HOST') else None
DISCOURSE_API_USER = os.environ.get('DISCOURSE_API_USER') if os.environ.get('DISCOURSE_HOST') else 'system'
REGISTRATION_OPEN = False # Disable built-in django registration - must register using forum
ROOT_URLCONF = 'PyRIGS.urls' ROOT_URLCONF = 'PyRIGS.urls'
WSGI_APPLICATION = 'PyRIGS.wsgi.application' WSGI_APPLICATION = 'PyRIGS.wsgi.application'
@@ -202,6 +228,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
"django.core.context_processors.tz", "django.core.context_processors.tz",
"django.core.context_processors.request", "django.core.context_processors.request",
"django.contrib.messages.context_processors.messages", "django.contrib.messages.context_processors.messages",
'social.apps.django_app.context_processors.backends',
'social.apps.django_app.context_processors.login_redirect',
) )

View File

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

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.utils import formats
from django.conf import settings from django.conf import settings
from django.core import serializers from django.core import serializers
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm from django.contrib.auth.forms import UserCreationForm, UserChangeForm, AuthenticationForm, PasswordResetForm
from registration.forms import RegistrationFormUniqueEmail from registration.forms import RegistrationFormUniqueEmail
from captcha.fields import ReCaptchaField from captcha.fields import ReCaptchaField
@@ -27,6 +28,16 @@ class ProfileRegistrationFormUniqueEmail(RegistrationFormUniqueEmail):
raise forms.ValidationError("These initials are already in use. Please supply different initials.") raise forms.ValidationError("These initials are already in use. Please supply different initials.")
return self.cleaned_data['initials'] return self.cleaned_data['initials']
def clean_first_name(self):
if self.cleaned_data["first_name"].strip() == '':
raise ValidationError("First name is required.")
return self.cleaned_data["first_name"]
def clean_last_name(self):
if self.cleaned_data["last_name"].strip() == '':
raise ValidationError("Last name is required.")
return self.cleaned_data["last_name"]
# Login form # Login form
class PasswordReset(PasswordResetForm): class PasswordReset(PasswordResetForm):

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) initials = models.CharField(max_length=5, unique=True, null=True, blank=False)
phone = models.CharField(max_length=13, null=True, blank=True) phone = models.CharField(max_length=13, null=True, blank=True)
api_key = models.CharField(max_length=40, blank=True, editable=False, null=True) api_key = models.CharField(max_length=40, blank=True, editable=False, null=True)
avatar_template = models.CharField(max_length=255, blank=True, editable=False, null=True)
@classmethod @classmethod
def make_api_key(cls): def make_api_key(cls):
@@ -33,8 +34,13 @@ class Profile(AbstractUser):
@property @property
def profile_picture(self): def profile_picture(self):
url = "" url = ""
if settings.DISCOURSE_API_KEY is not None:
if self.avatar_template:
return settings.DISCOURSE_HOST+self.avatar_template.format(size=500)
if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None: if settings.USE_GRAVATAR or settings.USE_GRAVATAR is None:
url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500" url = "https://www.gravatar.com/avatar/" + hashlib.md5(self.email).hexdigest() + "?d=wavatar&s=500"
return url return url
@property @property

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 KiB

View File

@@ -133,6 +133,20 @@
{% endif %} {% endif %}
</dd> </dd>
</dl> </dl>
<h4>Linked to {{object.social_auth.count}} Forum Account(s)</h4>
{% if object.social_auth.count > 0 %}
<a href="{% url 'unlink_forum' %}" class="btn btn-danger">
Unlink Forum Account(s) <span class="glyphicon glyphicon-pencil"></span>
</a>
{% else %}
<a href="{% url "social:begin" "discourse" %}" class="btn btn-success">
Link Forum Account <span class="glyphicon glyphicon-pencil"></span>
</a>
{% endif %}
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

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

View File

@@ -153,6 +153,7 @@ urlpatterns = patterns('',
url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()), url(r'^user/edit/$', login_required(views.ProfileUpdateSelf.as_view()),
name='profile_update_self'), name='profile_update_self'),
url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'), url(r'^user/reset_api_key$', login_required(views.ResetApiKey.as_view(permanent=False)), name='reset_api_key'),
url(r'^user/unlink_forum$', login_required(views.UnlinkForum.as_view(permanent=False)), name='unlink_forum'),
# ICS Calendar - API key authentication # ICS Calendar - API key authentication
url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"), url(r'^ical/(?P<api_pk>\d+)/(?P<api_key>\w+)/rigs.ics$', api_key_required(ical.CalendarICS()), name="ics_calendar"),

View File

@@ -382,3 +382,12 @@ class ResetApiKey(generic.RedirectView):
self.request.user.save() self.request.user.save()
return reverse_lazy('profile_detail') return reverse_lazy('profile_detail')
class UnlinkForum(generic.RedirectView):
def get_redirect_url(self, *args, **kwargs):
for link in self.request.user.social_auth.all():
link.delete()
self.request.user.save()
return reverse_lazy('profile_detail')

View File

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

View File

@@ -1,4 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static from staticfiles %}
{% block title %}Login{% endblock %} {% block title %}Login{% endblock %}
@@ -6,5 +7,43 @@
<div class="text-center"> <div class="text-center">
<h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1> <h1>R<small>ig</small> I<small>nformation</small> G<small>athering</small> S<small>ystem</small></h1>
</div> </div>
<div class="panel-group">
{% url "social:complete" "discourse" as completeUrl %}
{% if not request.GET.next == completeUrl %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
Login with TEC Forum
</h4>
</div>
<div id="forumLogin">
<div class="panel-body" style="text-align:center;">
<a class="btn btn-default" href="{% url "social:begin" "discourse" %}?next={{request.GET.next}}">
<h4>Login using</h4>
<img src="{% static "imgs/forum-logo.gif" %}" width=200></img>
</a>
</div>
</div>
</div>
{% endif %}
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
Login with RIGS Credentials
</h4>
</div>
<div class="panel-body">
<div class="panel-body">
{% include 'registration/loginform.html' %} {% include 'registration/loginform.html' %}
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}

View File

@@ -13,7 +13,7 @@
{% render_field form.password class+="form-control" placeholder=form.password.label %} {% render_field form.password class+="form-control" placeholder=form.password.label %}
</div> </div>
<div class="text-right"> <div class="text-right">
<a href="{% url 'registration_register' %}" class="btn">Register</a> {# <a href="{% url 'registration_register' %}" class="btn">Register</a> #}
<a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a> <a href="{% url 'password_reset' %}" class="btn">Forgotten Password</a>
<input type="submit" value="Login" class="btn btn-primary"/> <input type="submit" value="Login" class="btn btn-primary"/>
<input type="hidden" name="next" value="{{ next }}"/> <input type="hidden" name="next" value="{{ next }}"/>