Commit b340c2fd by Peter Fogg

Allow users to request access to the Course Discovery API.

ECOM-3940
parent 9543989e
...@@ -325,6 +325,5 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd' ...@@ -325,6 +325,5 @@ SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
INSTALLED_APPS += ('openedx.core.djangoapps.ccxcon',) INSTALLED_APPS += ('openedx.core.djangoapps.ccxcon',)
FEATURES['CUSTOM_COURSES_EDX'] = True FEATURES['CUSTOM_COURSES_EDX'] = True
# API access management. Necessary so that django-simple-history # API access management -- needed for simple-history to run.
# doesn't break when running pre-test migrations.
INSTALLED_APPS += ('openedx.core.djangoapps.api_admin',) INSTALLED_APPS += ('openedx.core.djangoapps.api_admin',)
...@@ -795,3 +795,5 @@ CREDENTIALS_GENERATION_ROUTING_KEY = HIGH_PRIORITY_QUEUE ...@@ -795,3 +795,5 @@ CREDENTIALS_GENERATION_ROUTING_KEY = HIGH_PRIORITY_QUEUE
# The extended StudentModule history table # The extended StudentModule history table
if FEATURES.get('ENABLE_CSMH_EXTENDED'): if FEATURES.get('ENABLE_CSMH_EXTENDED'):
INSTALLED_APPS += ('coursewarehistoryextended',) INSTALLED_APPS += ('coursewarehistoryextended',)
API_ACCESS_MANAGER_EMAIL = ENV_TOKENS.get('API_ACCESS_MANAGER_EMAIL')
...@@ -2856,3 +2856,7 @@ DEFAULT_SITE_ID = 1 ...@@ -2856,3 +2856,7 @@ DEFAULT_SITE_ID = 1
# Cache time out settings # Cache time out settings
# by Comprehensive Theme system # by Comprehensive Theme system
THEME_CACHE_TIMEOUT = 30 * 60 THEME_CACHE_TIMEOUT = 30 * 60
# API access management
API_ACCESS_MANAGER_EMAIL = 'api-access@example.com'
API_ACCESS_FROM_EMAIL = 'api-requests@example.com'
...@@ -59,6 +59,7 @@ ...@@ -59,6 +59,7 @@
@import 'views/bookmarks'; @import 'views/bookmarks';
@import 'course/auto-cert'; @import 'course/auto-cert';
@import 'views/program-list'; @import 'views/program-list';
@import 'views/api-access';
// app - discussion // app - discussion
@import "discussion/utilities/variables"; @import "discussion/utilities/variables";
......
...@@ -25,6 +25,7 @@ label { ...@@ -25,6 +25,7 @@ label {
textarea, textarea,
input[type="text"], input[type="text"],
input[type="url"],
input[type="email"], input[type="email"],
input[type="password"], input[type="password"],
input[type="tel"] { input[type="tel"] {
......
#api-access-wrapper {
#api-access-request-header {
@extend %t-title4;
margin-bottom: 0;
padding: $baseline;
@include text-align(left);
}
.request-status {
margin: 0 $baseline;
padding: $baseline;
box-shadow: 0 1px 2px 1px $shadow-l1;
&.request-pending {
border-top: 2px solid $orange;
}
}
#api-access-status {
@extend %t-copy-base;
}
#api-access-request {
padding: 0 $baseline $baseline $baseline;
#api-form-fields {
li {
margin: $baseline 0;
.helptext {
@extend %t-copy-sub1;
display: block;
}
}
label {
@extend %t-copy-base;
display: block;
font-style: normal;
}
input, textarea {
@extend %t-copy-base;
font-family: 'Open Sans';
font-style: normal;
}
.errorlist {
padding: 0;
list-style-type: none;
li {
@extend %t-copy-base;
margin: 0;
color: $red;
}
}
}
#api-access-submit {
@extend %t-copy-base;
border-radius: 3px;
border: none;
background-color: $blue;
box-shadow: none;
background-image: none;
text-shadow: none;
text-transform: none;
}
}
}
## mako
<%page expression_filter="h"/>
<%inherit file="../main.html"/>
<%
from django.utils.translation import ugettext as _
%>
<div id="api-access-wrapper">
<h1 id="api-access-request-header">${_("{platform_name} API Access Request").format(platform_name=settings.PLATFORM_NAME)}</h1>
<form action="" method="post" id="api-access-request">
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
<ul id="api-form-fields">
${form.as_ul() | n}
</ul>
<input id="api-access-submit" type="submit" value="${_('Request API Access')}" />
</form>
</div>
## mako
We have received the following request to use the Course Discovery API. Please go to ${approval_url} to approve the user.
Company name: ${company_name}
Company contact: ${username}
Company URL: ${url}
Address: ${company_address}
Reason for API usage: ${reason}
## mako
<%page expression_filter="h"/>
<%inherit file="../main.html"/>
<%
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
from openedx.core.djangolib.markup import Text, HTML
%>
<div id="api-access-wrapper">
<h1 id="api-access-request-header">${_("{platform_name} API Access Request").format(platform_name=settings.PLATFORM_NAME)}</h1>
<div class="request-status request-${status}">
% if status == ApiAccessRequest.PENDING:
## Translators: "platform_name" is the name of this Open edX installation. "link_start" and "link_end" are the HTML for a link to the API documentation. "api_support_email_link" is HTML for a link to email the API support staff.
<p id="api-access-status">${Text(_('Your request to access the {platform_name} Course Catalog API is being processed. You will receive a message at the email address in your profile when processing is complete. You can also return to this page to see the status of your API access request. To learn more about the {platform_name} Course Catalog API, visit {link_start}our API documentation page{link_end}. For questions about using this API, visit our FAQ page or contact {api_support_email_link}.')).format(
platform_name=Text(settings.PLATFORM_NAME),
link_start=HTML('<a href="{}">').format(Text(api_support_link)),
link_end=HTML('</a>'),
api_support_email_link=HTML('<a href="mailto:{email}">{email}</a>').format(email=Text(api_support_email))
)}</p>
% endif
## TODO (ECOM-3946): Add status text for 'active' and 'denied', as well as API client creation.
</div>
</div>
...@@ -995,3 +995,8 @@ if settings.FEATURES.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'): ...@@ -995,3 +995,8 @@ if settings.FEATURES.get('ENABLE_FINANCIAL_ASSISTANCE_FORM'):
name='submit_financial_assistance_request' name='submit_financial_assistance_request'
) )
) )
# URLs for API access management
urlpatterns += (
url(r'^api-admin/', include('openedx.core.djangoapps.api_admin.urls')),
)
"""Admin views for API managment.""" """Admin views for API managment."""
from django.contrib import admin from django.contrib import admin
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest from config_models.admin import ConfigurationModelAdmin
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, ApiAccessConfig
@admin.register(ApiAccessRequest) @admin.register(ApiAccessRequest)
...@@ -10,3 +11,7 @@ class ApiAccessRequestAdmin(admin.ModelAdmin): ...@@ -10,3 +11,7 @@ class ApiAccessRequestAdmin(admin.ModelAdmin):
list_display = ('user', 'status', 'website') list_display = ('user', 'status', 'website')
list_filter = ('status',) list_filter = ('status',)
search_fields = ('user__email',) search_fields = ('user__email',)
raw_id_fields = ('user',)
admin.site.register(ApiAccessConfig, ConfigurationModelAdmin)
"""Decorators for API access management."""
from functools import wraps
from django.http import HttpResponseNotFound
from openedx.core.djangoapps.api_admin.models import ApiAccessConfig
def api_access_enabled_or_404(view):
"""If API access management feature is not enabled, return a 404."""
@wraps(view)
def wrapped_view(request, *args, **kwargs):
"""Wrapper for the view function."""
if ApiAccessConfig.current().enabled:
return view(request, *args, **kwargs)
return HttpResponseNotFound()
return wrapped_view
"""Forms for API management."""
from django import forms
from django.conf import settings
from django.utils.translation import ugettext as _
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
class ApiAccessRequestForm(forms.ModelForm):
"""Form to request API access."""
terms_of_service = forms.BooleanField(
label=_('{platform_name} API Terms of Service').format(platform_name=settings.PLATFORM_NAME),
help_text=_(
'The resulting Package will still be considered part of Covered Code. Your Grants.'
' In consideration of, and distributed, a Modification is: (a) any addition to or loss'
' of data, programs or other fee is charged for the physical act of transferring a copy,'
' and you may do so by its licensors. The Licensor grants to You for damages, including '
'any direct, indirect, special, incidental and consequential damages, such as lost profits;'
' iii) states that any such claim is resolved (such as deliberate and grossly negligent acts)'
' or agreed to in writing, the Copyright Holder nor by the laws of the Original Code and'
' any other entity based on the same media as an expression of character texts or the whole'
' of the Licensed Product, and (iv) you make to the general goal of allowing unrestricted '
're-use and re-distribute applies to "Community Portal Server" and related software products'
' as well as in related documentation and collateral materials stating that you have modified'
' that component; or it may be copied, modified, distributed, and/or redistributed.'
),
)
class Meta(object):
model = ApiAccessRequest
fields = ('company_name', 'website', 'company_address', 'reason', 'terms_of_service')
labels = {
'reason': _('Describe what your application does.'),
}
help_texts = {
'reason': None,
'website': _("The URL of your company's website."),
'company_name': _('The name of your company.'),
'company_address': _('The contact address of your company.'),
}
widgets = {
'company_address': forms.Textarea()
}
# -*- 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),
('api_admin', '0002_auto_20160325_1604'),
]
operations = [
migrations.CreateModel(
name='ApiAccessConfig',
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,
},
),
migrations.AddField(
model_name='apiaccessrequest',
name='company_address',
field=models.CharField(default=b'', max_length=255),
),
migrations.AddField(
model_name='apiaccessrequest',
name='company_name',
field=models.CharField(default=b'', max_length=255),
),
migrations.AddField(
model_name='historicalapiaccessrequest',
name='company_address',
field=models.CharField(default=b'', max_length=255),
),
migrations.AddField(
model_name='historicalapiaccessrequest',
name='company_name',
field=models.CharField(default=b'', max_length=255),
),
migrations.AlterField(
model_name='apiaccessrequest',
name='user',
field=models.OneToOneField(to=settings.AUTH_USER_MODEL),
),
]
...@@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _ ...@@ -7,6 +7,7 @@ from django.utils.translation import ugettext as _
from django_extensions.db.models import TimeStampedModel from django_extensions.db.models import TimeStampedModel
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
from config_models.models import ConfigurationModel
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -22,7 +23,7 @@ class ApiAccessRequest(TimeStampedModel): ...@@ -22,7 +23,7 @@ class ApiAccessRequest(TimeStampedModel):
(DENIED, _('Denied')), (DENIED, _('Denied')),
(APPROVED, _('Approved')), (APPROVED, _('Approved')),
) )
user = models.ForeignKey(User) user = models.OneToOneField(User)
status = models.CharField( status = models.CharField(
max_length=255, max_length=255,
choices=STATUS_CHOICES, choices=STATUS_CHOICES,
...@@ -32,6 +33,8 @@ class ApiAccessRequest(TimeStampedModel): ...@@ -32,6 +33,8 @@ class ApiAccessRequest(TimeStampedModel):
) )
website = models.URLField(help_text=_('The URL of the website associated with this API user.')) website = models.URLField(help_text=_('The URL of the website associated with this API user.'))
reason = models.TextField(help_text=_('The reason this user wants to access the API.')) reason = models.TextField(help_text=_('The reason this user wants to access the API.'))
company_name = models.CharField(max_length=255, default='')
company_address = models.CharField(max_length=255, default='')
history = HistoricalRecords() history = HistoricalRecords()
...@@ -45,7 +48,24 @@ class ApiAccessRequest(TimeStampedModel): ...@@ -45,7 +48,24 @@ class ApiAccessRequest(TimeStampedModel):
Returns: Returns:
bool bool
""" """
return cls.objects.filter(user=user, status=cls.APPROVED).exists() return cls.api_access_status(user) == cls.APPROVED
@classmethod
def api_access_status(cls, user):
"""
Returns the user's API access status, or None if they have not
requested access.
Arguments:
user (User): The user to check access for.
Returns:
str or None
"""
try:
return cls.objects.get(user=user).status
except cls.DoesNotExist:
return None
def approve(self): def approve(self):
"""Approve this request.""" """Approve this request."""
...@@ -61,3 +81,10 @@ class ApiAccessRequest(TimeStampedModel): ...@@ -61,3 +81,10 @@ class ApiAccessRequest(TimeStampedModel):
def __unicode__(self): def __unicode__(self):
return u'ApiAccessRequest {website} [{status}]'.format(website=self.website, status=self.status) return u'ApiAccessRequest {website} [{status}]'.format(website=self.website, status=self.status)
class ApiAccessConfig(ConfigurationModel):
"""Configuration for API management."""
def __unicode__(self):
return u'ApiAccessConfig [enabled={}]'.format(self.enabled)
#pylint: disable=missing-docstring
import ddt
from django.test import TestCase
from openedx.core.djangoapps.api_admin.forms import ApiAccessRequestForm
from openedx.core.djangoapps.api_admin.tests.utils import VALID_DATA
@ddt.ddt
class ApiAccessFormTest(TestCase):
@ddt.data(
(VALID_DATA, True),
({}, False),
(dict(VALID_DATA, terms_of_service=False), False)
)
@ddt.unpack
def test_form_valid(self, data, is_valid):
form = ApiAccessRequestForm(data)
self.assertEqual(form.is_valid(), is_valid)
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
import ddt import ddt
from django.db import IntegrityError
from django.test import TestCase from django.test import TestCase
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, ApiAccessConfig
from openedx.core.djangoapps.api_admin.tests.factories import ApiAccessRequestFactory from openedx.core.djangoapps.api_admin.tests.factories import ApiAccessRequestFactory
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
...@@ -42,3 +43,29 @@ class ApiAccessRequestTests(TestCase): ...@@ -42,3 +43,29 @@ class ApiAccessRequestTests(TestCase):
self.request.status = status self.request.status = status
self.request.save() # pylint: disable=no-member self.request.save() # pylint: disable=no-member
self.assertEqual(ApiAccessRequest.has_api_access(self.user), should_have_access) self.assertEqual(ApiAccessRequest.has_api_access(self.user), should_have_access)
def test_unique_per_user(self):
with self.assertRaises(IntegrityError):
ApiAccessRequestFactory(user=self.user)
def test_no_access(self):
self.request.delete() # pylint: disable=no-member
self.assertIsNone(ApiAccessRequest.api_access_status(self.user))
def test_unicode(self):
request_unicode = unicode(self.request)
self.assertIn(self.request.website, request_unicode) # pylint: disable=no-member
self.assertIn(self.request.status, request_unicode)
class ApiAccessConfigTests(TestCase):
def test_unicode(self):
self.assertEqual(
unicode(ApiAccessConfig(enabled=True)),
u'ApiAccessConfig [enabled=True]'
)
self.assertEqual(
unicode(ApiAccessConfig(enabled=False)),
u'ApiAccessConfig [enabled=False]'
)
#pylint: disable=missing-docstring
from smtplib import SMTPException
import unittest
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import TestCase
import mock
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest, ApiAccessConfig
from openedx.core.djangoapps.api_admin.tests.factories import ApiAccessRequestFactory
from openedx.core.djangoapps.api_admin.tests.utils import VALID_DATA
from openedx.core.djangoapps.api_admin.views import log as view_log
from student.tests.factories import UserFactory
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class ApiRequestViewTest(TestCase):
def setUp(self):
super(ApiRequestViewTest, self).setUp()
self.url = reverse('api-request')
password = 'abc123'
self.user = UserFactory(password=password)
self.client.login(username=self.user.username, password=password)
ApiAccessConfig(enabled=True).save()
def test_get(self):
"""Verify that a logged-in can see the API request form."""
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_get_anonymous(self):
"""Verify that users must be logged in to see the page."""
self.client.logout()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302)
def test_get_with_existing_request(self):
"""
Verify that users who have already requested access are redirected
to the client creation page to see their status.
"""
ApiAccessRequestFactory(user=self.user)
response = self.client.get(self.url)
self.assertRedirects(response, reverse('api-status'))
def _assert_post_success(self, response):
"""
Assert that a successful POST has been made, that the response
redirects correctly, and that the correct object has been created.
"""
self.assertRedirects(response, reverse('api-status'))
api_request = ApiAccessRequest.objects.get(user=self.user)
self.assertEqual(api_request.status, ApiAccessRequest.PENDING)
return api_request
def test_post_valid(self):
"""Verify that a logged-in user can create an API request."""
self.assertFalse(ApiAccessRequest.objects.all().exists())
with mock.patch('openedx.core.djangoapps.api_admin.views.send_mail') as mock_send_mail:
response = self.client.post(self.url, VALID_DATA)
mock_send_mail.assert_called_once_with(
'API access request from ' + VALID_DATA['company_name'],
mock.ANY,
settings.API_ACCESS_FROM_EMAIL,
[settings.API_ACCESS_MANAGER_EMAIL],
fail_silently=False
)
self._assert_post_success(response)
def test_failed_email(self):
"""
Verify that an access request is still created if sending email
fails for some reason, and that the necessary information is
logged.
"""
mail_function = 'openedx.core.djangoapps.api_admin.views.send_mail'
with mock.patch(mail_function, side_effect=SMTPException):
with mock.patch.object(view_log, 'exception') as mock_view_log_exception:
response = self.client.post(self.url, VALID_DATA)
api_request = self._assert_post_success(response)
mock_view_log_exception.assert_called_once_with(
'Error sending API request email for request [%s].', api_request.id # pylint: disable=no-member
)
def test_post_anonymous(self):
"""Verify that users must be logged in to create an access request."""
self.client.logout()
with mock.patch('openedx.core.djangoapps.api_admin.views.send_mail') as mock_send_mail:
response = self.client.post(self.url, VALID_DATA)
mock_send_mail.assert_not_called()
self.assertEqual(response.status_code, 302)
self.assertFalse(ApiAccessRequest.objects.all().exists())
def test_get_with_feature_disabled(self):
"""Verify that the view can be disabled via ApiAccessConfig."""
ApiAccessConfig(enabled=False).save()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
def test_post_with_feature_disabled(self):
"""Verify that the view can be disabled via ApiAccessConfig."""
ApiAccessConfig(enabled=False).save()
response = self.client.post(self.url)
self.assertEqual(response.status_code, 404)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class ApiRequestStatusViewTest(TestCase):
def setUp(self):
super(ApiRequestStatusViewTest, self).setUp()
ApiAccessConfig(enabled=True).save()
password = 'abc123'
self.user = UserFactory(password=password)
self.client.login(username=self.user.username, password=password)
self.url = reverse('api-status')
def test_get_without_request(self):
"""
Verify that users who have not yet requested API access are
redirected to the API request form.
"""
response = self.client.get(self.url)
self.assertRedirects(response, reverse('api-request'))
def test_get_with_request(self):
"""
Verify that users who have requested access can see a message
regarding their request status.
"""
ApiAccessRequestFactory(user=self.user)
response = self.client.get(self.url)
self.assertEqual(response.status_code, 200)
def test_get_anonymous(self):
"""Verify that users must be logged in to see the page."""
self.client.logout()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 302)
def test_get_with_feature_disabled(self):
"""Verify that the view can be disabled via ApiAccessConfig."""
ApiAccessConfig(enabled=False).save()
response = self.client.get(self.url)
self.assertEqual(response.status_code, 404)
"""Common utilities for tests."""
VALID_DATA = {
'website': 'https://example.com',
'reason': 'I like APIs',
'company_address': '141 Portland Street, Cambridge MA 02139',
'company_name': 'BreadX',
'terms_of_service': True,
}
"""URLs for API access management."""
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from openedx.core.djangoapps.api_admin.decorators import api_access_enabled_or_404
from openedx.core.djangoapps.api_admin.views import ApiRequestView, ApiRequestStatusView
urlpatterns = (
url(
r'^status$',
api_access_enabled_or_404(login_required(ApiRequestStatusView.as_view())),
name="api-status"
),
url(
r'$',
api_access_enabled_or_404(login_required(ApiRequestView.as_view())),
name="api-request"
),
)
"""Views for API management."""
import logging
from smtplib import SMTPException
from django.conf import settings
from django.core.mail import send_mail
from django.core.urlresolvers import reverse_lazy, reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext as _
from django.views.generic import View
from django.views.generic.edit import CreateView
from edxmako.shortcuts import render_to_response, render_to_string
from openedx.core.djangoapps.api_admin.forms import ApiAccessRequestForm
from openedx.core.djangoapps.api_admin.models import ApiAccessRequest
log = logging.getLogger(__name__)
class ApiRequestView(CreateView):
"""Form view for requesting API access."""
form_class = ApiAccessRequestForm
template_name = 'api_admin/api_access_request_form.html'
success_url = reverse_lazy('api-status')
def get(self, request):
"""
If the requesting user has already requested API access, redirect
them to the client creation page.
"""
if ApiAccessRequest.api_access_status(request.user) is not None:
return redirect(reverse('api-status'))
return super(ApiRequestView, self).get(request)
def send_email(self, api_request):
"""
Send an email to settings.API_ACCESS_MANAGER_EMAIL with the
contents of this API access request.
"""
context = {
'approval_url': self.request.build_absolute_uri(
reverse('admin:api_admin_apiaccessrequest_change', args=(api_request.id,))
),
'company_name': api_request.company_name,
'username': api_request.user.username,
'url': api_request.website,
'company_address': api_request.company_address,
'reason': api_request.reason,
}
message = render_to_string('api_admin/email.txt', context)
try:
send_mail(
_('API access request from {company}').format(company=api_request.company_name),
message,
settings.API_ACCESS_FROM_EMAIL,
[settings.API_ACCESS_MANAGER_EMAIL],
fail_silently=False
)
except SMTPException:
log.exception('Error sending API request email for request [%s].', api_request.id)
def form_valid(self, form):
form.instance.user = self.request.user
result = super(ApiRequestView, self).form_valid(form)
self.send_email(form.instance)
return result
class ApiRequestStatusView(View):
"""View for confirming our receipt of an API request."""
def get(self, request):
"""
If the user has not created an API request, redirect them to the
request form. Otherwise, display the status of their API request.
"""
status = ApiAccessRequest.api_access_status(request.user)
if status is None:
return redirect(reverse('api-request'))
return render_to_response('api_admin/status.html', {
'status': status,
'api_support_link': _('TODO'),
'api_support_email': settings.API_ACCESS_MANAGER_EMAIL,
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment