Added custom user model to store extra data from the GED + corrected signals and api auth class so they interface correctly with the ged
authordurandn
Thu, 03 Mar 2016 17:06:20 +0100
changeset 11 cfc868991b82
parent 10 50b532f5e6cb
child 12 76bcfd35a363
Added custom user model to store extra data from the GED + corrected signals and api auth class so they interface correctly with the ged
server/src/metaeducation/admin.py
server/src/metaeducation/auth.py
server/src/metaeducation/migrations/0001_initial.py
server/src/metaeducation/migrations/__init__.py
server/src/metaeducation/models.py
server/src/metaeducation/mtdc_oauth_provider/views.py
server/src/metaeducation/settings/__init__.py
server/src/metaeducation/settings/dev.py.tmpl
server/src/metaeducation/signals.py
--- /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