Merge pull request #12651 from edx/clintonb/single-logout
Added support for OpenID Connect single logout
This commit is contained in:
@@ -5,6 +5,7 @@ from ratelimitbackend import admin
|
||||
|
||||
from cms.djangoapps.contentstore.views.program import ProgramAuthoringView, ProgramsIdTokenView
|
||||
from cms.djangoapps.contentstore.views.organization import OrganizationListView
|
||||
from student.views import LogoutView
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
@@ -65,7 +66,7 @@ urlpatterns += patterns(
|
||||
|
||||
# ajax view that actually does the work
|
||||
url(r'^login_post$', 'student.views.login_user', name='login_post'),
|
||||
url(r'^logout$', 'student.views.logout_user', name='logout'),
|
||||
url(r'^logout$', LogoutView.as_view(), name='logout'),
|
||||
)
|
||||
|
||||
# restful api
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
""" Django template ontext processors. """
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from microsite_configuration import microsite
|
||||
|
||||
|
||||
def microsite_context(request): # pylint: disable=missing-docstring,unused-argument
|
||||
return {
|
||||
'platform_name': microsite.get_value('platform_name', settings.PLATFORM_NAME)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
""" Tests for Django template context processors. """
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from microsite_configuration.context_processors import microsite_context
|
||||
|
||||
PLATFORM_NAME = 'Test Platform'
|
||||
|
||||
|
||||
@override_settings(PLATFORM_NAME=PLATFORM_NAME)
|
||||
class MicrositeContextProcessorTests(TestCase):
|
||||
""" Tests for the microsite context processor. """
|
||||
|
||||
def setUp(self):
|
||||
super(MicrositeContextProcessorTests, self).setUp()
|
||||
request = RequestFactory().get('/')
|
||||
self.context = microsite_context(request)
|
||||
|
||||
def test_platform_name(self):
|
||||
""" Verify the context includes the platform name. """
|
||||
self.assertEqual(self.context['platform_name'], PLATFORM_NAME)
|
||||
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('student', '0005_auto_20160531_1653'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LogoutViewConfiguration',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('change_date', models.DateTimeField(auto_now_add=True, verbose_name='Change date')),
|
||||
('enabled', models.BooleanField(default=False, verbose_name='Enabled')),
|
||||
('changed_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, editable=False, to=settings.AUTH_USER_MODEL, null=True, verbose_name='Changed by')),
|
||||
],
|
||||
options={
|
||||
'ordering': ('-change_date',),
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -2216,3 +2216,11 @@ class UserAttribute(TimeStampedModel):
|
||||
return cls.objects.get(user=user, name=name).value
|
||||
except cls.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
class LogoutViewConfiguration(ConfigurationModel):
|
||||
""" Configuration for the logout view. """
|
||||
|
||||
def __unicode__(self):
|
||||
"""Unicode representation of the instance. """
|
||||
return u'Logout view configuration: {enabled}'.format(enabled=self.enabled)
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
"""
|
||||
Test the student dashboard view.
|
||||
"""
|
||||
import ddt
|
||||
import unittest
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.test import TestCase
|
||||
from edx_oauth2_provider.constants import AUTHORIZED_CLIENTS_SESSION_KEY
|
||||
from edx_oauth2_provider.tests.factories import ClientFactory, TrustedClientFactory
|
||||
from mock import patch
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from student.models import CourseEnrollment
|
||||
from student.helpers import DISABLE_UNENROLL_CERT_STATES
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
|
||||
from student.helpers import DISABLE_UNENROLL_CERT_STATES
|
||||
from student.models import CourseEnrollment, LogoutViewConfiguration
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
|
||||
PASSWORD = 'test'
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
@@ -22,9 +27,6 @@ class TestStudentDashboardUnenrollments(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test to ensure that the student dashboard does not show the unenroll button for users with certificates.
|
||||
"""
|
||||
USERNAME = "Bob"
|
||||
EMAIL = "bob@example.com"
|
||||
PASSWORD = "edx"
|
||||
UNENROLL_ELEMENT_ID = "#actions-item-unenroll-0"
|
||||
|
||||
@classmethod
|
||||
@@ -35,10 +37,10 @@ class TestStudentDashboardUnenrollments(SharedModuleStoreTestCase):
|
||||
def setUp(self):
|
||||
""" Create a course and user, then log in. """
|
||||
super(TestStudentDashboardUnenrollments, self).setUp()
|
||||
self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
|
||||
self.user = UserFactory()
|
||||
CourseEnrollmentFactory(course_id=self.course.id, user=self.user)
|
||||
self.cert_status = None
|
||||
self.client.login(username=self.USERNAME, password=self.PASSWORD)
|
||||
self.client.login(username=self.user.username, password=PASSWORD)
|
||||
|
||||
def mock_cert(self, _user, _course_overview, _course_mode):
|
||||
""" Return a preset certificate status. """
|
||||
@@ -107,3 +109,89 @@ class TestStudentDashboardUnenrollments(SharedModuleStoreTestCase):
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
class LogoutTests(TestCase):
|
||||
""" Tests for the logout functionality. """
|
||||
|
||||
def setUp(self):
|
||||
""" Create a course and user, then log in. """
|
||||
super(LogoutTests, self).setUp()
|
||||
self.user = UserFactory()
|
||||
self.client.login(username=self.user.username, password=PASSWORD)
|
||||
LogoutViewConfiguration.objects.create(enabled=True)
|
||||
|
||||
def create_oauth_client(self):
|
||||
""" Creates a trusted OAuth client. """
|
||||
client = ClientFactory(logout_uri='https://www.example.com/logout/')
|
||||
TrustedClientFactory(client=client)
|
||||
return client
|
||||
|
||||
def assert_session_logged_out(self, oauth_client, **logout_headers):
|
||||
""" Authenticates a user via OAuth 2.0, logs out, and verifies the session is logged out. """
|
||||
self.authenticate_with_oauth(oauth_client)
|
||||
|
||||
# Logging out should remove the session variables, and send a list of logout URLs to the template.
|
||||
# The template will handle loading those URLs and redirecting the user. That functionality is not tested here.
|
||||
response = self.client.get(reverse('logout'), **logout_headers)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(AUTHORIZED_CLIENTS_SESSION_KEY, self.client.session)
|
||||
|
||||
return response
|
||||
|
||||
def authenticate_with_oauth(self, oauth_client):
|
||||
""" Perform an OAuth authentication using the current web client.
|
||||
|
||||
This should add an AUTHORIZED_CLIENTS_SESSION_KEY entry to the current session.
|
||||
"""
|
||||
data = {
|
||||
'client_id': oauth_client.client_id,
|
||||
'client_secret': oauth_client.client_secret,
|
||||
'response_type': 'code'
|
||||
}
|
||||
# Authenticate with OAuth to set the appropriate session values
|
||||
self.client.post(reverse('oauth2:capture'), data, follow=True)
|
||||
self.assertListEqual(self.client.session[AUTHORIZED_CLIENTS_SESSION_KEY], [oauth_client.client_id])
|
||||
|
||||
def assert_logout_redirects(self):
|
||||
""" Verify logging out redirects the user to the homepage. """
|
||||
response = self.client.get(reverse('logout'))
|
||||
self.assertRedirects(response, '/', fetch_redirect_response=False)
|
||||
|
||||
def test_switch(self):
|
||||
""" Verify the IDA logout functionality is disabled if the associated switch is disabled. """
|
||||
LogoutViewConfiguration.objects.create(enabled=False)
|
||||
oauth_client = self.create_oauth_client()
|
||||
self.authenticate_with_oauth(oauth_client)
|
||||
self.assert_logout_redirects()
|
||||
|
||||
def test_without_session_value(self):
|
||||
""" Verify logout works even if the session does not contain an entry with
|
||||
the authenticated OpenID Connect clients."""
|
||||
self.assert_logout_redirects()
|
||||
|
||||
def test_client_logout(self):
|
||||
""" Verify the context includes a list of the logout URIs of the authenticated OpenID Connect clients.
|
||||
|
||||
The list should only include URIs of the clients for which the user has been authenticated.
|
||||
"""
|
||||
client = self.create_oauth_client()
|
||||
response = self.assert_session_logged_out(client)
|
||||
expected = {
|
||||
'logout_uris': [client.logout_uri + '?no_redirect=1'], # pylint: disable=no-member
|
||||
'target': '/',
|
||||
}
|
||||
self.assertDictContainsSubset(expected, response.context_data) # pylint: disable=no-member
|
||||
|
||||
def test_filter_referring_service(self):
|
||||
""" Verify that, if the user is directed to the logout page from a service, that service's logout URL
|
||||
is not included in the context sent to the template.
|
||||
"""
|
||||
client = self.create_oauth_client()
|
||||
response = self.assert_session_logged_out(client, HTTP_REFERER=client.logout_uri) # pylint: disable=no-member
|
||||
expected = {
|
||||
'logout_uris': [],
|
||||
'target': '/',
|
||||
}
|
||||
self.assertDictContainsSubset(expected, response.context_data) # pylint: disable=no-member
|
||||
|
||||
@@ -7,12 +7,14 @@ import uuid
|
||||
import json
|
||||
import warnings
|
||||
from collections import defaultdict
|
||||
from urlparse import urljoin
|
||||
from urlparse import urljoin, urlsplit, parse_qs, urlunsplit
|
||||
|
||||
from django.views.generic import TemplateView
|
||||
from pytz import UTC
|
||||
from requests import HTTPError
|
||||
from ipware.ip import get_ip
|
||||
|
||||
import edx_oauth2_provider
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout, authenticate, login
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
@@ -21,22 +23,21 @@ from django.contrib.auth.views import password_reset_confirm
|
||||
from django.contrib import messages
|
||||
from django.core.context_processors import csrf
|
||||
from django.core import mail
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch, reverse_lazy
|
||||
from django.core.validators import validate_email, ValidationError
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.http import (HttpResponse, HttpResponseBadRequest, HttpResponseForbidden,
|
||||
HttpResponseServerError, Http404)
|
||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseForbidden, HttpResponseServerError, Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.encoding import force_bytes, force_text
|
||||
from django.utils.translation import ungettext
|
||||
from django.utils.http import base36_to_int, urlsafe_base64_encode
|
||||
from django.utils.http import base36_to_int, urlsafe_base64_encode, urlencode
|
||||
from django.utils.translation import ugettext as _, get_language
|
||||
from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie
|
||||
from django.views.decorators.http import require_POST, require_GET
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver, Signal
|
||||
from django.template.response import TemplateResponse
|
||||
|
||||
from provider.oauth2.models import Client
|
||||
from ratelimitbackend.exceptions import RateLimitException
|
||||
|
||||
from social.apps.django_app import utils as social_utils
|
||||
@@ -52,7 +53,8 @@ from student.models import (
|
||||
PendingEmailChange, CourseEnrollment, CourseEnrollmentAttribute, unique_id_for_user,
|
||||
CourseEnrollmentAllowed, UserStanding, LoginFailures,
|
||||
create_comments_service_user, PasswordHistory, UserSignupSource,
|
||||
DashboardConfiguration, LinkedInAddToProfileConfiguration, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED)
|
||||
DashboardConfiguration, LinkedInAddToProfileConfiguration, ManualEnrollmentAudit, ALLOWEDTOENROLL_TO_ENROLLED,
|
||||
LogoutViewConfiguration)
|
||||
from student.forms import AccountCreationForm, PasswordResetFormNoActive, get_registration_extension_form
|
||||
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
|
||||
from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error
|
||||
@@ -68,7 +70,6 @@ from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
@@ -733,7 +734,7 @@ def dashboard(request):
|
||||
'denied_banner': denied_banner,
|
||||
'billing_email': settings.PAYMENT_SUPPORT_EMAIL,
|
||||
'user': user,
|
||||
'logout_url': reverse(logout_user),
|
||||
'logout_url': reverse('logout'),
|
||||
'platform_name': platform_name,
|
||||
'enrolled_courses_either_paid': enrolled_courses_either_paid,
|
||||
'provider_states': [],
|
||||
@@ -1357,27 +1358,6 @@ def login_oauth_token(request, backend):
|
||||
raise Http404
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
def logout_user(request):
|
||||
"""
|
||||
HTTP request to log out the user. Redirects to marketing page.
|
||||
Deletes both the CSRF and sessionid cookies so the marketing
|
||||
site can determine the logged in state of the user
|
||||
"""
|
||||
# We do not log here, because we have a handler registered
|
||||
# to perform logging on successful logouts.
|
||||
request.is_from_logout = True
|
||||
logout(request)
|
||||
if settings.FEATURES.get('AUTH_USE_CAS'):
|
||||
target = reverse('cas-logout')
|
||||
else:
|
||||
target = '/'
|
||||
response = redirect(target)
|
||||
|
||||
delete_logged_in_cookies(response)
|
||||
return response
|
||||
|
||||
|
||||
@require_GET
|
||||
@login_required
|
||||
@ensure_csrf_cookie
|
||||
@@ -2486,3 +2466,74 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
|
||||
log.warning('Program structure is invalid, skipping display: %r', program)
|
||||
|
||||
return programs_data
|
||||
|
||||
|
||||
class LogoutView(TemplateView):
|
||||
"""
|
||||
Logs out user and redirects.
|
||||
|
||||
The template should load iframes to log the user out of OpenID Connect services.
|
||||
See http://openid.net/specs/openid-connect-logout-1_0.html.
|
||||
"""
|
||||
oauth_client_ids = []
|
||||
template_name = 'logout.html'
|
||||
|
||||
# Keep track of the page to which the user should ultimately be redirected.
|
||||
target = reverse_lazy('cas-logout') if settings.FEATURES.get('AUTH_USE_CAS') else '/'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs): # pylint: disable=missing-docstring
|
||||
# We do not log here, because we have a handler registered to perform logging on successful logouts.
|
||||
request.is_from_logout = True
|
||||
|
||||
# Get the list of authorized clients before we clear the session.
|
||||
self.oauth_client_ids = request.session.get(edx_oauth2_provider.constants.AUTHORIZED_CLIENTS_SESSION_KEY, [])
|
||||
|
||||
logout(request)
|
||||
|
||||
# If we don't need to deal with OIDC logouts, just redirect the user.
|
||||
if LogoutViewConfiguration.current().enabled and self.oauth_client_ids:
|
||||
response = super(LogoutView, self).dispatch(request, *args, **kwargs)
|
||||
else:
|
||||
response = redirect(self.target)
|
||||
|
||||
# Clear the cookie used by the edx.org marketing site
|
||||
delete_logged_in_cookies(response)
|
||||
|
||||
return response
|
||||
|
||||
def _build_logout_url(self, url):
|
||||
"""
|
||||
Builds a logout URL with the `no_redirect` query string parameter.
|
||||
|
||||
Args:
|
||||
url (str): IDA logout URL
|
||||
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
scheme, netloc, path, query_string, fragment = urlsplit(url)
|
||||
query_params = parse_qs(query_string)
|
||||
query_params['no_redirect'] = 1
|
||||
new_query_string = urlencode(query_params, doseq=True)
|
||||
return urlunsplit((scheme, netloc, path, new_query_string, fragment))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(LogoutView, self).get_context_data(**kwargs)
|
||||
|
||||
# Create a list of URIs that must be called to log the user out of all of the IDAs.
|
||||
uris = Client.objects.filter(client_id__in=self.oauth_client_ids,
|
||||
logout_uri__isnull=False).values_list('logout_uri', flat=True)
|
||||
|
||||
referrer = self.request.META.get('HTTP_REFERER', '').strip('/')
|
||||
logout_uris = []
|
||||
|
||||
for uri in uris:
|
||||
if not referrer or (referrer and not uri.startswith(referrer)):
|
||||
logout_uris.append(self._build_logout_url(uri))
|
||||
|
||||
context.update({
|
||||
'target': self.target,
|
||||
'logout_uris': logout_uris,
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
@@ -499,6 +499,7 @@ TEMPLATES = [
|
||||
'django.template.context_processors.i18n',
|
||||
'django.contrib.auth.context_processors.auth', # this is required for admin
|
||||
'django.template.context_processors.csrf',
|
||||
'microsite_configuration.context_processors.microsite_context',
|
||||
|
||||
# Added for django-wiki
|
||||
'django.template.context_processors.media',
|
||||
|
||||
33
lms/static/js/jquery.allloaded.js
Normal file
33
lms/static/js/jquery.allloaded.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* jQuery plugin that waits until all elements are loaded before executing the specified function.
|
||||
*
|
||||
* Adapted from http://stackoverflow.com/a/35777807/592820.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* $('iframe').allLoaded(function () {
|
||||
* window.alert('All iframes loaded!');
|
||||
* });
|
||||
*
|
||||
*/
|
||||
|
||||
;(function ($) {
|
||||
'use strict';
|
||||
|
||||
$.fn.extend({
|
||||
allLoaded: function (fn) {
|
||||
var $elems = this;
|
||||
var waiting = this.length;
|
||||
|
||||
var handler = function () {
|
||||
--waiting;
|
||||
if (!waiting) {
|
||||
fn.call(window);
|
||||
}
|
||||
this.unbind(handler);
|
||||
};
|
||||
|
||||
return $elems.load(handler);
|
||||
}
|
||||
});
|
||||
})(jQuery);
|
||||
23
lms/static/js/logout.js
Normal file
23
lms/static/js/logout.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* JS for the logout page.
|
||||
*
|
||||
* This script waits for all iframes on the page to load before redirecting the user
|
||||
* to a specified URL. If there are no iframes on the page, the user is immediately redirected.
|
||||
*/
|
||||
(function ($) {
|
||||
'use strict';
|
||||
|
||||
$(function () {
|
||||
var $iframeContainer = $('#iframeContainer'),
|
||||
$iframes = $iframeContainer.find('iframe'),
|
||||
redirectUrl = $iframeContainer.data('redirect-url');
|
||||
|
||||
if ($iframes.length === 0) {
|
||||
window.location = redirectUrl;
|
||||
}
|
||||
|
||||
$iframes.allLoaded(function () {
|
||||
window.location = redirectUrl;
|
||||
});
|
||||
});
|
||||
})(jQuery);
|
||||
23
lms/templates/logout.html
Normal file
23
lms/templates/logout.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "main_django.html" %}
|
||||
{% load i18n staticfiles %}
|
||||
|
||||
{% block title %}{% trans "Signed Out" %} | {{ block.super }}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<h1>{% trans "You have signed out." %}</h1>
|
||||
|
||||
<p style="text-align: center; margin-bottom: 20px;">
|
||||
{% blocktrans %}
|
||||
If you are not redirected within 5 seconds, <a href="{{ target }}">click here to go to the home page</a>.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<div id="iframeContainer" style="visibility: hidden" data-redirect-url="{{ target }}">
|
||||
{% for uri in logout_uris %}
|
||||
<iframe src="{{ uri }}"></iframe>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="{% static 'js/jquery.allLoaded.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'js/logout.js' %}"></script>
|
||||
{% endblock body %}
|
||||
@@ -4,7 +4,7 @@
|
||||
<html lang="{{LANGUAGE_CODE}}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
{% block title %}<title>{% platform_name %}</title>{% endblock %}
|
||||
<title>{% block title %}{{ platform_name }}{% endblock %}</title>
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="{% favicon_path %}" />
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
{% with online_help_token="wiki" %}
|
||||
{% load pipeline %}{% load sekizai_tags i18n microsite %}{% load url from future %}{% load staticfiles %}
|
||||
|
||||
{% block title %}<title>{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}</title>{% endblock %}
|
||||
{% block title %}
|
||||
{% block pagetitle %}{% endblock %} | {% trans "Wiki" %} | {% platform_name %}
|
||||
{% endblock %}
|
||||
|
||||
{% block bodyclass %}view-in-course view-wiki{% endblock %}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from config_models.views import ConfigurationModelCurrentAPIView
|
||||
from courseware.views.index import CoursewareIndex
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.self_paced.models import SelfPacedConfiguration
|
||||
from student.views import LogoutView
|
||||
|
||||
# Uncomment the next two lines to enable the admin:
|
||||
if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'):
|
||||
@@ -42,7 +43,7 @@ urlpatterns = (
|
||||
url(r'^accounts/disable_account_ajax$', 'student.views.disable_account_ajax',
|
||||
name="disable_account_ajax"),
|
||||
|
||||
url(r'^logout$', 'student.views.logout_user', name='logout'),
|
||||
url(r'^logout$', LogoutView.as_view(), name='logout'),
|
||||
url(r'^create_account$', 'student.views.create_account', name='create_account'),
|
||||
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name="activate"),
|
||||
|
||||
|
||||
@@ -41,9 +41,9 @@ djangorestframework-oauth==1.1.0
|
||||
edx-ccx-keys==0.1.2
|
||||
edx-drf-extensions==0.5.1
|
||||
edx-lint==0.4.3
|
||||
edx-django-oauth2-provider==1.0.3
|
||||
edx-django-oauth2-provider==1.1.1
|
||||
edx-django-sites-extensions==2.0.1
|
||||
edx-oauth2-provider==1.0.1
|
||||
edx-oauth2-provider==1.1.1
|
||||
edx-opaque-keys==0.2.1
|
||||
edx-organizations==0.4.1
|
||||
edx-rest-api-client==1.2.1
|
||||
|
||||
Reference in New Issue
Block a user