Added custom user model to store extra data from the GED + corrected signals and api auth class so they interface correctly with the ged
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/metaeducation/admin.py Thu Mar 03 17:06:20 2016 +0100
@@ -0,0 +1,76 @@
+from django import forms
+from django.contrib import admin
+from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
+from django.contrib.auth.forms import ReadOnlyPasswordHashField
+
+from .models import User
+
+
+class UserCreationForm(forms.ModelForm):
+ """A form for creating new users. Includes all the required
+ fields, plus a repeated password."""
+ password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
+ password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
+
+ class Meta:
+ model = User
+ fields = ('external_id', 'username', 'first_name', 'last_name')
+
+ def clean_password2(self):
+ # Check that the two password entries match
+ password1 = self.cleaned_data.get("password1")
+ password2 = self.cleaned_data.get("password2")
+ if password1 and password2 and password1 != password2:
+ raise forms.ValidationError("Passwords don't match")
+ return password2
+
+ def save(self, commit=True):
+ # Save the provided password in hashed format
+ user = super(UserCreationForm, self).save(commit=False)
+ user.set_password(self.cleaned_data["password1"])
+ if commit:
+ user.save()
+ return user
+
+
+class UserChangeForm(forms.ModelForm):
+ """A form for updating users. Includes all the fields on
+ the user, but replaces the password field with admin's
+ password hash display field.
+ """
+ password = ReadOnlyPasswordHashField()
+
+ class Meta:
+ model = User
+ fields = ('external_id', 'username', 'first_name', 'last_name', 'password', 'is_active', 'is_staff')
+
+
+class UserAdmin(BaseUserAdmin):
+ # The forms to add and change user instances
+ form = UserChangeForm
+ add_form = UserCreationForm
+
+ # The fields to be used in displaying the User model.
+ # These override the definitions on the base UserAdmin
+ # that reference specific fields on auth.User.
+ list_display = ('external_id', 'username', 'first_name', 'last_name', 'password', 'is_staff')
+ list_filter = ('is_staff',)
+ fieldsets = (
+ (None, {'fields': ('external_id', 'username')}),
+ ('Personal info', {'fields': ('username', 'first_name', 'last_name')}),
+ ('Permissions', {'fields': ('is_staff',)}),
+ )
+ # add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
+ # overrides get_fieldsets to use this attribute when creating a user.
+ add_fieldsets = (
+ (None, {
+ 'classes': ('wide',),
+ 'fields': ('external_id', 'username', 'first_name', 'last_name', 'password1', 'password2')}
+ ),
+ )
+ search_fields = ('username',)
+ ordering = ('username',)
+ filter_horizontal = ()
+
+# Now register the new UserAdmin
+admin.site.register(User, UserAdmin)
\ No newline at end of file
--- a/server/src/metaeducation/auth.py Thu Mar 03 16:28:46 2016 +0100
+++ b/server/src/metaeducation/auth.py Thu Mar 03 17:06:20 2016 +0100
@@ -2,6 +2,7 @@
from django.contrib.auth import get_user_model, login
from django.contrib.auth.models import Permission
from django.conf import settings
+from urllib import parse
import requests
import re
import json
@@ -10,22 +11,22 @@
def authenticate(self, request):
# get token, get username
- if 'act_as' not in request.GET or 'HTTP_RENKAN_ACT_AS' not in request.META:
+ if ("act_as" not in request.GET) and ('HTTP_RENKAN_ACT_AS' not in request.META):
return
else:
- username = request.GET.get('act_as', request.META.get("HTTP_RENKAN_ACT_AS", ""))
+ external_id = request.GET.get('act_as', request.META.get("HTTP_RENKAN_ACT_AS", ""))
try:
- user = get_user_model().objects.get(username=username)
+ user = get_user_model().objects.get(external_id=external_id)
except get_user_model().DoesNotExist:
return
if 'HTTP_AUTHORIZATION' not in request.META:
return
else:
token = re.search("(?<=\s).*", request.META["HTTP_AUTHORIZATION"]).group(0)
-
+ print(token)
# send token to Oauth server
token_validate_response = requests.get(
- settings.MTDC_VALIDATE_TOKEN_URL+token
+ settings.MTDC_VALIDATE_TOKEN_BASE_URL+token+"?redirect_uri="+parse.quote_plus(settings.MTDC_GED_BASE_URL)
)
if token_validate_response.status_code != 200:
return
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/server/src/metaeducation/migrations/0001_initial.py Thu Mar 03 17:06:20 2016 +0100
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.9.1 on 2016-03-03 10:40
+from __future__ import unicode_literals
+
+import django.core.validators
+from django.db import migrations, models
+import django.utils.timezone
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ('auth', '0007_alter_validators_add_error_messages'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='User',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
+ ('external_id', models.CharField(default=uuid.uuid4, max_length=256, unique=True)),
+ ('username', models.CharField(max_length=150, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.')])),
+ ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
+ ('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
+ ('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
+ ('is_active', models.BooleanField(default=True)),
+ ('is_staff', models.BooleanField(default=False)),
+ ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
+ ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ ]
--- a/server/src/metaeducation/models.py Thu Mar 03 16:28:46 2016 +0100
+++ b/server/src/metaeducation/models.py Thu Mar 03 17:06:20 2016 +0100
@@ -1,8 +1,64 @@
-from django.contrib.auth.models import AbstractUser
-from django.db.models import CharField
+from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
+from django.db import models
+from django.core import validators
+from django.utils.translation import ugettext_lazy as _
+from django.utils import timezone
+
+import uuid
+
+class UserManager(BaseUserManager):
+
+ def create_user(self, external_id, username, password=None, **extra_fields):
+ """
+ Creates and saves a User with the given username, email and password.
+ """
+ now = timezone.now()
+ user = self.model(external_id=external_id, username=username,
+ is_staff=False, is_active=True, is_superuser=False,
+ date_joined=now, **extra_fields)
+ user.set_password(password)
+ user.save(using=self._db)
+ return user
-class User(AbstractUser):
- external_id = CharField(max_length=256, blank=True, null=True)
+ def create_superuser(self, external_id, username, password=None, **extra_fields):
+ superuser = self.create_user(external_id, username, password, **extra_fields)
+ superuser.is_staff = True
+ superuser.is_active = True
+ superuser.is_superuser = True
+ superuser.save(using=self._db)
+ return superuser
+
+class User(AbstractBaseUser, PermissionsMixin):
+ external_id = models.CharField(max_length=256, unique=True, default=uuid.uuid4)
+ username = models.CharField(max_length=150,
+ validators=[
+ validators.RegexValidator(
+ r'^[\w.@+-]+$',
+ _('Enter a valid username. This value may contain only '
+ 'letters, numbers ' 'and @/./+/-/_ characters.')
+ ),
+ ],
+ )
+ first_name = models.CharField(_('first name'), max_length=30, blank=True)
+ last_name = models.CharField(_('last name'), max_length=30, blank=True)
+ date_joined = models.DateTimeField(default=timezone.now)
+ is_active = models.BooleanField(default=True)
+ is_staff = models.BooleanField(default=False)
+
+ objects = UserManager()
+
+ USERNAME_FIELD = 'external_id'
+ REQUIRED_FIELDS = ['username']
+
+ def get_full_name(self):
+ """
+ Returns the first_name plus the last_name, with a space in between.
+ """
+ full_name = '%s %s' % (self.first_name, self.last_name)
+ return full_name.strip()
+
+ def get_short_name(self):
+ return self.first_name
def __unicode__(self):
return self.external_id
\ No newline at end of file
--- a/server/src/metaeducation/mtdc_oauth_provider/views.py Thu Mar 03 16:28:46 2016 +0100
+++ b/server/src/metaeducation/mtdc_oauth_provider/views.py Thu Mar 03 17:06:20 2016 +0100
@@ -86,7 +86,6 @@
def complete_login(self, request, app, token, **kwargs):
resp = requests.get(self.profile_url,
params={'access_token': token.token})
- print(resp.text)
extra_data = resp.json()
if request.session.get("OAUTH_CONTEXT_BASE_URL", None) is not None:
del request.session["OAUTH_CONTEXT_BASE_URL"]
--- a/server/src/metaeducation/settings/__init__.py Thu Mar 03 16:28:46 2016 +0100
+++ b/server/src/metaeducation/settings/__init__.py Thu Mar 03 17:06:20 2016 +0100
@@ -119,13 +119,11 @@
ANONYMOUS_USER_ID = -1
DEFAULT_RENKAN_ICON = ""
+RENKAN_USER_DISPLAY_FIELD = 'external_id'
-#ACCOUNT_AUTHENTICATION_METHOD = "username"
-#ACCOUNT_ADAPTER = "allauth.account.adapter.DefaultAccountAdapter"
SOCIALACCOUNT_ADAPTER = "metaeducation.mtdc_oauth_provider.views.MtdcOAuth2Adapter"
SOCIALACCOUNT_PROVIDERS = {
'mtdc': {
'SCOPE': ['basic']
}
}
-
--- a/server/src/metaeducation/settings/dev.py.tmpl Thu Mar 03 16:28:46 2016 +0100
+++ b/server/src/metaeducation/settings/dev.py.tmpl Thu Mar 03 17:06:20 2016 +0100
@@ -38,14 +38,17 @@
ANONYMOUS_USER_ID = -1
DEFAULT_RENKAN_ICON = ""
+RENKAN_TOOL_ID = "" # Renkan ID, provided by ITOP
MTDC_ACCESS_TOKEN_URL = "" # This URL is the access token endpoint URL, relative to a <CONTEXT> Base url that will be passed as query arg to the server
MTDC_AUTHORIZE_URL = "" # This URL is the authorize endpoint URL, relative to a <CONTEXT> Base url that will be passed as query arg to the server
MTDC_PROFILE_URL = "" # This URL is the user profile endpoint URL, relative to a <CONTEXT> Base url that will be passed as query arg to the server
+MTDC_GED_BASE_URL = "" # Base URL of the GED server
MTDC_VALIDATE_TOKEN_URL = "" # This URL is the ABSOLUTE url for validating a token. There will be no context involved for validation token from server to server.
-MTDC_REFERENCE_RESOURCE_BASE_URL = "" # This URL is the ABSOLUTE url for referencing a resource (ged webservice)
MTDC_CLIENT_CREDENTIALS_TOKEN_URL = "" # This URL is the ABSOLUTE url for getting a token via Client Credentials from server to server.
+MTDC_REFERENCE_RESOURCE_BASE_URL = "" # This URL is the ABSOLUTE url for the GED resource referencement service from server to server.
+OAUTH_REDIRECT_URI = "" # Redirect URI for the GED server, used when validating GED tokens during client credentials flow.
-MTDC_USERNAME_FIELD_NAME = "username" # Name of the username field that will be transmitted in response to the profile url request (usually "username", for metaeduc will probably be "displayName")
+MTDC_USERNAME_FIELD_NAME = "displayName" # Name of the username field that will be transmitted in response to the profile url request (usually "username", for metaeduc will probably be "displayName")
MTDC_USERID_FIELD_NAME = "id" # Name of the user id field that will be transmitted in response to the profile url request (usually "id")
\ No newline at end of file
--- a/server/src/metaeducation/signals.py Thu Mar 03 16:28:46 2016 +0100
+++ b/server/src/metaeducation/signals.py Thu Mar 03 17:06:20 2016 +0100
@@ -2,9 +2,9 @@
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from django.conf import settings
-import requests, json
+from django.core.urlresolvers import reverse
+import requests, json, sys
-@receiver(post_save)
def reference_created_renkan(sender, instance, created, **kwargs):
from renkanmanager.models import Renkan
if created and sender == Renkan and not instance.source_revision_guid:
@@ -18,14 +18,25 @@
if token_response.status_code == 200:
token = json.loads(token_response.text)['access_token']
post_data = json.dumps({
- "userId": instance.creator.external_id,
+ "id": str(instance.renkan_guid),
+ "idOutil": settings.RENKAN_TOOL_ID,
+ "idUser": instance.creator.external_id,
+ "pf": "ITOPPP",
+ "uai": "0310000W",
+ "redirect_uri": settings.OAUTH_REDIRECT_URI,
"title": instance.title
})
- print(post_data)
+ resource_ws_url = settings.MTDC_REFERENCE_RESOURCE_BASE_URL# + str(instance.renkan_guid)
+
reference_response = requests.post(
- settings.MTDC_REFERENCE_RESOURCE_BASE_URL+str(instance.renkan_guid),
+ resource_ws_url,
data = post_data,
headers = {
- "Authorization": "Bearer %s" % token
+ "Authorization": "Bearer %s" % token,
+ "content-type": "application/json"
}
- )
\ No newline at end of file
+ )
+
+
+if not 'test' in sys.argv:
+ post_save.connect(reference_created_renkan)
\ No newline at end of file