Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0245db1
badge assertion add cert url as evidence for course groups
bryanlandia May 22, 2017
1c1250f
Make sure multi-course course meta badge event groups still send awards
bryanlandia Aug 31, 2017
2bacc1e
hotfix external course task name
melvinsoft Sep 6, 2017
cded392
Rework BadgeClass slugs and lookups
bryanlandia Sep 6, 2017
fb6c091
Merge badges changes allowing evidence url for single course course g…
bryanlandia Sep 6, 2017
38eaf4f
new user update endpoint
melvinsoft Sep 8, 2017
9b2b566
Merge pull request #167 from appsembler/maxi/maxi/new-user-update-end…
melvinsoft Sep 25, 2017
aab9f3e
Merge pull request #160 from appsembler/appsembler/ficus/feature/badg…
bryanlandia Sep 26, 2017
8d82a86
Update advanced settings JS to multichoice settings fields
bryanlandia Sep 27, 2017
2bb20b9
Specify MEDIA_ROOT, MEDIA_URL in cms env (used for Scorm)
bryanlandia Oct 2, 2017
6294d66
set https off if passing http base scheme in env tokens
bryanlandia Oct 2, 2017
f6f97d8
Merge pull request #171 from appsembler/appsembler/ficus/hotfix/cms-m…
bryanlandia Oct 6, 2017
87a9aaa
Merge pull request #169 from appsembler/appsembler/ficus/hotfix/fix-m…
bryanlandia Oct 6, 2017
fa82658
use AUTHENTICATION_BACKENDS from APPSEMBLER_FEATURES
tkeemon Sep 28, 2017
70c9a5f
Merge pull request #170 from appsembler/appsembler/ficus/feature/auth…
tkeemon Oct 8, 2017
9174386
Fix multichoice adv settings JS
bryanlandia Oct 9, 2017
a05f9fa
try to import user-org backend and create obj at registration time
tkeemon Oct 18, 2017
6b25443
bypass auto login after registration with APPSEMBLER_FEATURE flag
tkeemon Oct 18, 2017
425efc5
conditionally import our custom org middleware
tkeemon Oct 18, 2017
0dbd8c5
fix formatting; include exception type in try/except
tkeemon Oct 19, 2017
e8fd8a0
remove unnecessary try/except
tkeemon Oct 19, 2017
d06202e
Merge pull request #174 from appsembler/appsembler/ficus/feature/nyif…
tkeemon Oct 19, 2017
5356bf2
More pythonic HTTPS conditional from ENV_TOKENS in aws_appsembler
bryanlandia Oct 20, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cms/envs/aws_appsembler.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,9 @@
del DATABASES['appsembler_usage']

CUSTOM_SSO_FIELDS_SYNC = ENV_TOKENS.get('CUSTOM_SSO_FIELDS_SYNC', {})

# set media values for SCORM upload
MEDIA_ROOT = '/edx/var/edxapp/media'
MEDIA_URL = '/media/'

HTTPS = 'on' if ENV_TOKENS.get('BASE_SCHEME', 'https').lower() == 'https' else 'off'
35 changes: 28 additions & 7 deletions cms/static/js/views/settings/advanced.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@ define(['js/views/validation',
// Model class is CMS.Models.Settings.Advanced
events: {
'focus :input': 'focusInput',
'blur :input': 'blurInput'
'blur :input': 'blurInput',
'change select': 'clearValidationErrors',
'change select': 'selectSetField'
// TODO enable/disable save based on validation (currently enabled whenever there are changes)
},
initialize: function() {
this.template = HtmlUtils.template(
$('#advanced_entry-tpl').text()
);
initialize : function() {
this.base_template = HtmlUtils.template(
$("#advanced_entry-tpl").text()
);
this.options_template = HtmlUtils.template(
$("#advanced_option_entry-tpl").text()
);
this.listenTo(this.model, 'invalid', this.handleValidationError);
this.render();
},
render: function() {
// catch potential outside call before template loaded
if (!this.template) return this;
if (!this.base_template) return this;

var listEle$ = this.$el.find('.course-advanced-policy-list');
listEle$.empty();
Expand Down Expand Up @@ -150,9 +155,11 @@ define(['js/views/validation',
});
},
renderTemplate: function(key, model) {
var tmpl = (model.values) ? this.options_template : this.base_template;
var newKeyId = _.uniqueId('policy_key_'),
newEle = this.template({key: key, display_name: model.display_name, help: model.help,
newEle = tmpl({key: key, display_name: model.display_name, help: model.help,
value: JSON.stringify(model.value, null, 4), deprecated: model.deprecated,
options: model.values,
keyUniqueId: newKeyId, valueUniqueId: _.uniqueId('policy_value_')});

this.fieldToSelectorMap[key] = newKeyId;
Expand All @@ -164,7 +171,21 @@ define(['js/views/validation',
},
blurInput: function(event) {
$(event.target).prev().removeClass('is-focused');
},
selectSetField : function(event) {
var self = this;
var key = this.selectorToField[event.currentTarget.id];
var newVal = $(event.currentTarget).val();
var JSONValue = JSON.parse(newVal);
var modelVal = self.model.get(key);
modelVal.value = JSONValue;
self.model.set(key, modelVal);
var message = gettext("Your changes will not take effect until you save your progress. Take care with key and value formatting, as validation is not implemented.");
self.showNotificationBar(message,
_.bind(self.saveView, self),
_.bind(self.revertView, self));
}

});

return AdvancedView;
Expand Down
17 changes: 15 additions & 2 deletions common/djangoapps/student/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,11 @@
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
from openedx.core.djangoapps.catalog.utils import get_programs_data

# try to import appsembler fork of edx-organizations (if it's installed)
try:
from organizations.models import UserOrganizationMapping
except ImportError:
pass

log = logging.getLogger("edx.student")
AUDIT_LOG = logging.getLogger("audit")
Expand Down Expand Up @@ -1857,13 +1862,21 @@ def create_account_with_params(request, params):
else:
registration.activate()
_enroll_user_in_pending_courses(user) # Enroll student in any pending courses

#if using custom Appsembler backend from edx-organizations
if u'organizations.backends.OrganizationMemberBackend' in settings.AUTHENTICATION_BACKENDS:
organization = request.site.organizations.first()
if organization:
UserOrganizationMapping.objects.get_or_create(user=user, organization=organization, is_active=False)

# Immediately after a user creates an account, we log them in. They are only
# logged in until they close the browser. They can't log in again until they click
# the activation link from the email.
new_user = authenticate(username=user.username, password=params['password'])
login(request, new_user)
request.session.set_expiry(0)

if not settings.APPSEMBLER_FEATURES.get('SKIP_LOGIN_AFTER_REGISTRATION', False):
login(request, new_user)
request.session.set_expiry(0)

try:
record_registration_attributions(request, new_user)
Expand Down
65 changes: 65 additions & 0 deletions lms/djangoapps/appsembler_api/apidocs.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,71 @@ Cache-Control: no-cache
}
```

### Update user account

This endpoint allows to update a user account. Receives a lookup parameter and N optional parameters which are all the attributes that needs to be updated.
The endpoint can update email, all available profile fields and also has support for [registration extension form fields](https://github.com/open-craft/custom-form-app).

* URL: `/appsembler_api/v0/accounts/update_user
* Method: `POST`
* Data Params
* Required:
* `user_lookup` # can be username or email
* Optional:
* `email` # user's email
* `name` # user full name
* `country` # country iso code, ex: `ES`, `UY`, `US`
* `gender` # user gender, accepted values `m`, `f` or `o`
* `level_of_education` # user education, accepted values `p`, `m`, `b`, `a`, `hs`, `jhs`, `el`, `none` or `other`,
* `year_of_birth` # four digit year as string
* `city` # text
* `mailing_address` # long text
* `language` # language iso code, ex 'ES', 'EN'
* `goals` # long text
* `bio` # text

You also can send extended profile form fiels, but that depends on every installation, you'll need to find the [registration extension form app fork](https://github.com/open-craft/custom-form-app) that is installed on the instance, and check the field names, and accepted values.

* Success Response
* Code: 200
* Content: Success message and list of updated fields and values
```
{
"success": "The following fields has been updated: name=Doe, John, country=ES"
}
```
* Error Responses:
* Code: 404 NOT FOUND
* Reason: User not exists

* Error Responses:
* Code: 400 NOT FOUND
* Reason: No user lookup parameter sent

* Example call:
```
POST /appsembler_api/v0/accounts/update_user
Host: example.com
Content-Type: application/json
Authorization: Bearer cbf6a5de322cf6a4323c957a882xy1s321c954b86
Cache-Control: no-cache
{
"user_lookup": "staff@example.com",
"emai": "new_staff@example.com",
"name": "Staff New Name",
"country": "US",
"gender": "f",
"level_of_education": "m",
"year_of_birth": "2000",
"city": "Montevideo",
"mailing_address": "A streen and a number 2345 FL, USA",
"language": "es",
"goals": "To be famous",
"bio": "I'm not famous yet"
"district": "101815" # a custom form field
}
```

### Check Existing Username

This endpoint is a tool to check if an user exists given the username.
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/appsembler_api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
url(r'^accounts/user_without_password', views.CreateUserAccountWithoutPasswordView.as_view(), name="create_user_account_without_password_api"),
url(r'^accounts/create', views.CreateUserAccountView.as_view(), name="create_user_account_api"),
url(r'^accounts/connect', views.UserAccountConnect.as_view(), name="user_account_connect_api"),
url(r'^accounts/update_user', views.UpdateUserAccount.as_view(), name="user_account_update_user"),
url(r'^accounts/get-user/(?P<username>[\w.+-]+)', views.GetUserAccountView.as_view(), name="get_user_account_api"),

# bulk enrollment API
Expand Down
106 changes: 104 additions & 2 deletions lms/djangoapps/appsembler_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.core.urlresolvers import reverse
from django.contrib.auth.models import User
from django.http import Http404
from django.db.models import Q
from django.core.validators import validate_email

from rest_framework.views import APIView
Expand All @@ -26,9 +27,10 @@
IsStaffOrOwner, ApiKeyHeaderPermissionIsAuthenticated
)

from student.forms import get_registration_extension_form
from student.views import create_account_with_params
from student.models import CourseEnrollment, EnrollmentClosedError, \
CourseFullError, AlreadyEnrolledError
CourseFullError, AlreadyEnrolledError, UserProfile

from course_modes.models import CourseMode
from courseware.courses import get_course_by_id
Expand Down Expand Up @@ -241,6 +243,107 @@ def post(self, request):
return response


class UpdateUserAccount(APIView):
""" HTTP endpoint for updating and user account """

authentication_classes = OAuth2AuthenticationAllowInactiveUser,
permission_classes = IsStaffOrOwner,

def post(self, request):
"""
This endpoint allows to change user attributes including email, profile
attributes and extended profile fields. Receives one mandatory param
user_lookup that can be an email or username to lookup the user to
update and the rest of parameters are option. Any attribute to update
must be sent in key:val JSON format.

URL: /appsembler_api/v0/accounts/update_user
Arguments:
request (HttpRequest)
JSON (application/json)
{
"user_lookup": email or username to lookup the user to update,
# mandatory ex: "staff4@example.com" or "staff4"

"email": "staff@example.com",
"bio": "this is my bio",
"country": "BR"
}
Returns:
HttpResponse: 200 on success, {"success ": "list of updated params"}
HttpResponse: 404 if the doesn't exists
HttpResponse: 400 Incorrect parameters, basically if username or
email parameter is not sent
"""
data = request.data

if data['user_lookup'].strip() == "":
errors = {"lookup_error": "No user lookup has been provided"}
return Response(errors, status=400)

user = User.objects.filter(
Q(username=data['user_lookup']) | Q(email=data['user_lookup'])
)

if user:
user = user[0]
else:
errors = {
"user_not_found": "The user for the Given username or email doesn't exists"
}
return Response(errors, status=404)

updated_fields = {}

# update email
if 'email' in data and data['email'] != user.email:
user_exists = check_account_exists(email=data['email'])
if user_exists:
errors = {"integrity_error": "the user email you're trying to set already belongs to another user"}
return Response(errors, status=400)

user.email = data['email']
user.save()
updated_fields.update({'email': data['email']})

# update profile fields
profile_fields = [
"name", "level_of_education", "gender", "mailing_address", "city",
"country", "goals", "bio", "year_of_birth", "language"
]

profile_fields_to_update = {}
for field in profile_fields:
if field in data:
profile_fields_to_update[field] = data[field]

if len(profile_fields_to_update):
UserProfile.objects.filter(user=user).update(**profile_fields_to_update)
updated_fields.update(profile_fields_to_update)

# If there is an exension form fields installed update them too
custom_profile_fields_to_update = {}
custom_form = get_registration_extension_form()

if custom_form is not None:
for custom_field in custom_form.fields:
if custom_field in data:
custom_profile_fields_to_update[custom_field] = data[custom_field]
updated_fields.update(custom_profile_fields_to_update)

if len(custom_profile_fields_to_update):
custom_form.Meta.model.objects.filter(user=user).update(
**custom_profile_fields_to_update)

return Response(
{"success": "The following fields has been updated: {}".format(
', '.join(
'{}={}'.format(f, v) for f, v in updated_fields.items())
)
},
status=200)


class GetUserAccountView(APIView):
authentication_classes = OAuth2AuthenticationAllowInactiveUser,
permission_classes = IsStaffOrOwner,
Expand All @@ -260,7 +363,6 @@ def get(self, request, username):
"""
try:
account_settings = User.objects.select_related('profile').get(username=username)
print account_settings
except User.DoesNotExist:
return Response(
status=status.HTTP_404_NOT_FOUND
Expand Down
29 changes: 10 additions & 19 deletions lms/djangoapps/badges/backends/badgr.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""
Badge Awarding backend for Badgr-Server.
"""
import hashlib
import logging
import mimetypes

import requests
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
from lazy import lazy
from requests.packages.urllib3.exceptions import HTTPError

Expand Down Expand Up @@ -57,20 +58,6 @@ def _assertion_url(self, slug):
"""
return "{}/assertions".format(self._badge_url(slug))

def _slugify(self, badge_class):
"""
Get a compatible badge slug from the specification.
"""
slug = badge_class.issuing_component + badge_class.slug
if badge_class.issuing_component and badge_class.course_id:
# Make this unique to the course, and down to 64 characters.
# We don't do this to badges without issuing_component set for backwards compatibility.
slug = hashlib.sha256(slug + unicode(badge_class.course_id)).hexdigest()
if len(slug) > MAX_SLUG_LENGTH:
# Will be 64 characters.
slug = hashlib.sha256(slug).hexdigest()
return slug

def _log_if_raised(self, response, data):
"""
Log server response if there was an error.
Expand Down Expand Up @@ -102,10 +89,14 @@ def _create_badge(self, badge_class):
u"Filename was: {}".format(image.name)
)
files = {'image': (image.name, image, content_type)}
try: # TODO: eventually we should pass both
URLValidator(badge_class.criteria)
criteria_type = 'criteria_url'
except ValidationError:
criteria_type = 'criteria_text'
data = {
'name': badge_class.display_name,
'criteria': badge_class.criteria,
'slug': self._slugify(badge_class),
criteria_type: badge_class.criteria,
'description': badge_class.description,
}
result = requests.post(
Expand Down Expand Up @@ -142,7 +133,7 @@ def _create_assertion(self, badge_class, user, evidence_url):
'evidence': evidence_url,
}
response = requests.post(
self._assertion_url(self._slugify(badge_class)), headers=self._get_headers(), data=data,
self._assertion_url(badge_class.slug), headers=self._get_headers(), data=data,
timeout=settings.BADGR_TIMEOUT
)
self._log_if_raised(response, data)
Expand All @@ -166,7 +157,7 @@ def _ensure_badge_created(self, badge_class):
"""
Verify a badge has been created for this badge class, and create it if not.
"""
slug = self._slugify(badge_class)
slug = badge_class.slug
if slug in BadgrBackend.badges:
return
response = requests.get(self._badge_url(slug), headers=self._get_headers(), timeout=settings.BADGR_TIMEOUT)
Expand Down
Loading