Skip to content

Commit

Permalink
Added Program Configuration Creation via API (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
Muhammad Haseeb authored Dec 3, 2020
1 parent 3121dc5 commit 376699b
Show file tree
Hide file tree
Showing 23 changed files with 611 additions and 4 deletions.
2 changes: 1 addition & 1 deletion credentials/apps/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class SiteConfigurationAdmin(admin.ModelAdmin):
search_fields = ('site__name',)
form = SiteConfigurationAdminForm
fieldsets = (
(None, {'fields': ('site', 'platform_name', 'company_name', 'segment_key', 'theme_name',
(None, {'fields': ('site', 'edx_org_short_name', 'platform_name', 'company_name', 'segment_key', 'theme_name',
'partner_from_address', 'records_enabled',)}),
(_('URLs'), {
'fields': (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.29 on 2020-11-30 07:23
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0020_siteconfiguration_edly_client_branding_and_django_settings'),
]

operations = [
migrations.AddField(
model_name='siteconfiguration',
name='edx_org_short_name',
field=models.CharField(help_text='Unique, short string identifier for organization, same as LMS. Please do not use spaces or special characters. Only allowed special characters are period (.), hyphen (-) and underscore (_).', max_length=255, null=True, unique=True, verbose_name='Organization short name'),
),
]
11 changes: 11 additions & 0 deletions credentials/apps/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@

class SiteConfiguration(models.Model):
site = models.OneToOneField(Site, null=False, blank=False)
edx_org_short_name = models.CharField(
max_length=255,
unique=True,
verbose_name=u'Organization short name',
help_text=_(
'Unique, short string identifier for organization, same as LMS. '
'Please do not use spaces or special characters. '
'Only allowed special characters are period (.), hyphen (-) and underscore (_).'
),
null=True,
)
platform_name = models.CharField(
verbose_name=_('Platform Name'),
help_text=_('Name of your Open edX platform'),
Expand Down
1 change: 1 addition & 0 deletions credentials/apps/core/tests/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class Meta(object):
model = SiteConfiguration

site = SubFactory(SiteFactory)
edx_org_short_name = Faker('word')
lms_url_root = Faker('url')
catalog_api_url = Faker('url')
platform_name = Faker('word')
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import base64

from django.core.files.base import ContentFile
from rest_framework import serializers


class Base64ImageField(serializers.ImageField):
"""
Custom serializer field to ingest base64 encoded image.
"""

def to_internal_value(self, data):
"""
Save base 64 encoded images.
SOURCE: http://matthewdaly.co.uk/blog/2015/07/04/handling-images-as-base64-strings-with-django-rest-framework/
"""
if not data:
return None

if isinstance(data, str) and data.startswith('data:image'):
file_format, image_string = data.split(';base64,') # format ~= data:image/X;base64,/xxxyyyzzz/
file_extension = file_format.split('/')[-1]
data = ContentFile(base64.b64decode(image_string), name='tmp.' + file_extension)

return super(Base64ImageField, self).to_internal_value(data)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""
Edly's Custom permissions classes for use with DRF.
"""
import logging

from rest_framework import permissions

from credentials.apps.edx_credentials_extensions.edly_credentials_app.utils import user_has_edx_organization_access

logger = logging.getLogger(__name__)


class CanAccessCurrentSite(permissions.BasePermission):
"""
Permission to check if the current site is allowed for the user.
"""

def has_permission(self, request, view):
"""
Checks for user's permission for current site.
"""
if user_has_edx_organization_access(request):
return True

return False
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Serializers for edly_api.
"""
from django.contrib.sites.models import Site

from rest_framework import serializers

from credentials.apps.credentials.models import ProgramCertificate, Signatory
from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.fields import Base64ImageField


class SignatorySerializer(serializers.ModelSerializer):
"""
Serializer for Signatory.
"""
image = Base64ImageField(max_length=None, use_url=True)

class Meta:
model = Signatory
fields = ['id', 'name', 'title', 'image']


class ProgramCertificateConfigurationSerializer(serializers.ModelSerializer):
"""
Serializer for Program Certificate Configuration.
"""
site = serializers.SlugRelatedField(slug_field='domain', queryset=Site.objects.all())
signatories = SignatorySerializer(many=True)

class Meta:
model = ProgramCertificate
fields = [
'site', 'is_active', 'signatories', 'program_uuid', 'use_org_name',
'include_hours_of_effort', 'language',
]

def create(self, validated_data):
signatories = []
for signatory in validated_data.pop('signatories', []):
signatories.append(Signatory.objects.create(**signatory))

program_certificate_configuration = ProgramCertificate(**validated_data)
program_certificate_configuration.save()
program_certificate_configuration.signatories.set(signatories)
return program_certificate_configuration

def update(self, instance, validated_data):
signatory_ids = self.context.get('signatory_ids', [])

signatories = []
for signatory_index, signatory in enumerate(validated_data.pop('signatories', [])):
signatory_obj, _ = Signatory.objects.update_or_create(
pk=signatory_ids[signatory_index],
defaults=signatory
)
signatories.append(signatory_obj)

for attr, value in validated_data.items():
setattr(instance, attr, value)

instance.save()
instance.signatories.set(signatories)
return instance
Empty file.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.conf.urls import include, url

urlpatterns = [
url(r'^v1/', include('credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.urls')),
]
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from rest_framework import routers

from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.v1.views.program_certificate_configuration import ProgramCertificateConfigurationViewSet


router = routers.SimpleRouter()
router.register(r'program-certificate-configuration', ProgramCertificateConfigurationViewSet, base_name='program-certificate-configuration')

urlpatterns = router.urls
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Views for Edly Program Certificate Configuration API.
"""
from rest_framework import status, viewsets
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

from credentials.apps.credentials.models import ProgramCertificate
from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.permissions import CanAccessCurrentSite
from credentials.apps.edx_credentials_extensions.edly_credentials_app.api.serializers import (
ProgramCertificateConfigurationSerializer
)


class ProgramCertificateConfigurationViewSet(viewsets.ModelViewSet):
"""
ViewSet for Program Certificate Configuration.
"""
lookup_field = 'program_uuid'
lookup_value_regex = '[0-9a-f-]+'
permission_classes = (IsAuthenticated, CanAccessCurrentSite)
serializer_class = ProgramCertificateConfigurationSerializer
queryset = ProgramCertificate.objects.all()

def update(self, request, *args, **kwargs):
"""
Update existing Program Certificate Configuration.
"""
data = request.data.copy()

signatory_ids = []
for signatory in data.get('signatories', []):
signatory_ids.append(signatory.get('id'))

context = {
'request': request,
'signatory_ids': signatory_ids
}

instance = ProgramCertificate.objects.get(program_uuid=kwargs.get('program_uuid'))

serializer = ProgramCertificateConfigurationSerializer(instance, data=data, context=context)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_200_OK, headers=headers)
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
Tests for edly_api Views.
"""
import json

from django.urls import reverse

from rest_framework.test import APITestCase

from credentials.apps.catalog.tests.factories import ProgramFactory
from credentials.apps.core.tests.factories import UserFactory
from credentials.apps.core.tests.mixins import SiteMixin
from credentials.apps.credentials.tests.factories import ProgramCertificateFactory

JSON_CONTENT_TYPE = 'application/json'


def get_test_image_as_base_64_encoded_string():
base64_header = 'data:image/png;base64,'
base64_data = (
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY'
'42YAAAAASUVORK5CYII='
)
base64_full = base64_header + base64_data
return base64_full


class ProgramCertificateConfigurationTests(SiteMixin, APITestCase):
"""
Test for Program Certificate Configuration Viewset.
"""

def setUp(self):
super(ProgramCertificateConfigurationTests, self).setUp()
self.list_url = reverse('edly_api:program-certificate-configuration-list')

self.user = UserFactory(is_staff=True, is_superuser=True)

program = ProgramFactory(site=self.site)
self.test_data = {
'site': self.site.domain,
'is_active': True,
'signatories': [
{
'name': 'Jhon Doe',
'title': 'Test Certificate',
'image': get_test_image_as_base_64_encoded_string()
}
],
'program_uuid': str(program.uuid),
'use_org_name': True,
'include_hours_of_effort': True,
'language': 'en'
}

def test_create_program_certificate_configuration(self):
"""
Verify program certificate configuration creation.
"""
response = self.client.post(self.list_url, data=json.dumps(self.test_data), content_type=JSON_CONTENT_TYPE)
assert response.status_code == 401

self.client.login(username=self.user.username, password='password')
response = self.client.post(self.list_url, data=json.dumps(self.test_data), content_type=JSON_CONTENT_TYPE)
assert response.status_code == 201

def test_update_program_certificate_configuration(self):
"""
Verify program certificate configuration update.
"""
program_certificate_configuration = ProgramCertificateFactory()

self.detail_url = reverse('edly_api:program-certificate-configuration-detail',
kwargs={'program_uuid': str(program_certificate_configuration.program_uuid)})
test_data = self.test_data
test_data['signatories'] = []
test_data['is_active'] = False

response = self.client.patch(self.detail_url, data=json.dumps(test_data), content_type=JSON_CONTENT_TYPE)
assert response.status_code == 401

self.client.login(username=self.user.username, password='password')
response = self.client.patch(self.detail_url, data=json.dumps(test_data), content_type=JSON_CONTENT_TYPE)
assert response.status_code == 200

def test_list_program_certificate_configuration(self):
"""
Verify list program certificate configuration.
"""
program_certificate_configuration = ProgramCertificateFactory()

response = self.client.get(self.list_url)
assert response.status_code == 401

self.client.login(username=self.user.username, password='password')
response = self.client.get(self.list_url)
assert response.status_code == 200

actual_data = response.json()
actual_data = actual_data.get('results')[0]
assert str(program_certificate_configuration.program_uuid) == actual_data.get('program_uuid')
Loading

0 comments on commit 376699b

Please sign in to comment.