Commit 72c3be00 by Bill DeRusha Committed by Bill DeRusha

Add Person API endpoint

Basic object create
Image upload
Nested position object create
parent f5da1339
import base64
from rest_framework import serializers
from django.core.files.base import ContentFile
class StdImageSerializerField(serializers.Field):
class StdImageSerializerField(serializers.ImageField):
"""
Custom serializer field to render out proper JSON representation of the StdImage field on model
"""
......@@ -21,9 +24,19 @@ class StdImageSerializerField(serializers.Field):
return serialized
def to_internal_value(self, obj):
""" We do not need to save/edit this banner image through serializer yet """
pass
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'):
# base64 encoded image - decode
file_format, imgstr = data.split(';base64,') # format ~= data:image/X;base64,/xxxyyyzzz/
ext = file_format.split('/')[-1] # guess file extension
data = ContentFile(base64.b64decode(imgstr), name='tmp.' + ext)
return super(StdImageSerializerField, self).to_internal_value(data)
class ImageField(serializers.Field): # pylint:disable=abstract-method
......
......@@ -201,12 +201,16 @@ class PositionSerializer(serializers.ModelSerializer):
class Meta(object):
model = Position
fields = ('title', 'organization_name',)
fields = ('title', 'organization_name', 'organization')
extra_kwargs = {
'organization': {'write_only': True}
}
class PersonSerializer(serializers.ModelSerializer):
"""Serializer for the ``Person`` model."""
position = PositionSerializer()
position = PositionSerializer(required=False)
profile_image = StdImageSerializerField(required=False)
@classmethod
def prefetch_queryset(cls):
......@@ -214,7 +218,19 @@ class PersonSerializer(serializers.ModelSerializer):
class Meta(object):
model = Person
fields = ('uuid', 'given_name', 'family_name', 'bio', 'profile_image_url', 'slug', 'position')
fields = (
'uuid', 'given_name', 'family_name', 'bio', 'profile_image_url', 'slug', 'position', 'profile_image',
'partner',
)
extra_kwargs = {
'partner': {'write_only': True}
}
def create(self, validated_data):
position_data = validated_data.pop('position')
person = Person.objects.create(**validated_data)
Position.objects.create(person=person, **position_data)
return person
class EndorsementSerializer(serializers.ModelSerializer):
......
# pylint: disable=no-member
import base64
import ddt
from django.core.files.base import ContentFile
from django.test import TestCase
from course_discovery.apps.api.fields import ImageField, StdImageSerializerField
......@@ -18,7 +23,7 @@ class ImageFieldTests(TestCase):
self.assertEqual(ImageField().to_representation(value), expected)
# pylint: disable=no-member
@ddt.ddt
class StdImageSerializerFieldTests(TestCase):
def test_to_representation(self):
request = make_request()
......@@ -40,3 +45,46 @@ class StdImageSerializerFieldTests(TestCase):
}
self.assertDictEqual(field.to_representation(program.banner_image), expected)
def test_to_internal_value(self):
base64_header = "data:image/jpeg;base64,"
base64_data = (
"/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAKBueIx4ZKCMgoy0qqC+8P//8Nzc8P/////////////////////"
"/////////////////////////////////////2wBDAaq0tPDS8P///////////////////////////////////////////////////////"
"///////////////////////wgARCADIAMgDASIAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECA//EABYBAQEBAAAAAAAAAAAAAAA"
"AAAABAv/aAAwDAQACEAMQAAABwADU6ZMtZFujDQzNwzbowtMNCZ68gAAADTI1kLci3I1INMjTI0yNZAAAAAAAAAAAAAAABrOpQgACStQAl"
"JrOoCUAACCyazpQgACDUWUSozrNKJQAAILJrOlGTUhAoClIsiCrZZQgACCyazSCgAALZRmiAtyjTJdIKyKiwAAACxQlEsAAAAAAAAAAAAA"
"AAAAAAAAAAAAAAAAAAAAADcMtDLQy3DLpzDcMunMNwy6cwAABZTpEBQg3lC43g1Lk6cunM65sLz6cwAAAAAAAABQAAAAA/8QAIBAAAwABB"
"AMBAQAAAAAAAAAAAAERQRAhMUACMEJwIP/aAAgBAQABBQL+ITabIhCEIJEIQhCE9Xz8rnOcZfHiYEZXOPTSl1pSlEylKUpS/gL7L7L7L7L"
"7L/TYTdjQkNEIRaREJpCEXoXJlnkfL4M5Z4i5zkZz6FplmwuGUyPlC51u/e//xAAUEQEAAAAAAAAAAAAAAAAAAABw/9oACAEDAQE/ASn/x"
"AAUEQEAAAAAAAAAAAAAAAAAAABw/9oACAECAQE/ASn/xAAUEAEAAAAAAAAAAAAAAAAAAACQ/9oACAEBAAY/Ahx//8QAJBAAAwABBAEEAwE"
"AAAAAAAAAAAERMRAhMEEgQFFhcWBwwfD/2gAIAQEAAT8h8HKydh2CUwPuYUzlHCyUHA9uRUsmcMomOFaK1wLJ76HoHgjsLH/dn8mQ7D3Q9"
"zDcSjEYjVC4FszMxkMZBZG66fUUKGVMMED6mYvgZ0yqQ9go+fxBZ4H6BizwP0JZ4H4QfIWeC+L0T4iz4UvGuEs6UvAtHquEs8r1XEvo6X9"
"bLdkiwCJCEhErIKoYe5I6BqMoQ3sRo02HuSOgefPAdizIvyYeAeI/YzIw0lkPIfX2NGjYPPnkVW0qEZo67GWwdSDSobUFVt0HSQ0FS3p2V"
"PsaQVW0e79HSopsVFKXSn//2gAMAwEAAgADAAAAEABMIOBEPCPCAAAEIAEIEEAAAAAAAAAAAAAAAAAAPP8A/wDwAIX/AP8A/wDD8/8A/wC"
"DCpP/AP8A/wAPzQuBBS4D3/8A/DzpBAMAYIX+wyMAAAAYIU8J7f7AAAAQQUgEpAz7AAAAAAAAAAAAAAAEMMMEEMAMAAAAAsQIQo0YY8AAA"
"AAAAAAA88cAA//EABwRAQADAAIDAAAAAAAAAAAAAAEAESAQMDFQYP/aAAgBAwEBPxD05h88mWGHkywly9GGD0DLly4/H//EABwRAQADAAI"
"DAAAAAAAAAAAAAAEAESAQMDFQYP/aAAgBAgEBPxD07g5ckcHKZIypWnzgidCXKlSvkP/EACoQAQACAQIEBQUBAQEAAAAAAAEAESExQRAwU"
"WEgcYGh4ZGxwdHwQGDx/9oACAEBAAE/EPBYOzt8x3PpGgv6S2l1HA3l2i6wFNoXNte3ep9EvT5l4lqmTmqlQRsZQnYdPmUi0vSCUmqmdQ9"
"e0xiwTMXXpyBYO8RUbJ+4/wB9ZoekdD6R+Wh7B+Z7wj+PWbDGc/Wfg/MODgsxEVGyfuKyDGz/AGYwFXBrElSsZ87gIhTfXvBT2ACJSjt41"
"Q1dS9msdL+IAsuefxAAV67/ABFQejK0FVF1gC7zOCyDzG1VG6N3eZOLuOgKA9ZfKsdL+JSU2LsghIu5iAHXvLaBUe135mOf+P0JUqVKlSp"
"XA7R147TeVKlSpUqVKlTbNLkapv4jTk7Jpcg5m8CAVpKQVK05WyaXjWo2ZvM1LgrtxdTk7JpeBBGbXeXweAZmWIhmPB8jZNLg0inxXwEI+"
"X2TSi0XNYTeb+J/swxMniqZr49kGm5nqRDZ478ThrBEzFtxpHgeC+kvFMtg1FvXlHA1m3A1/wAY8d+O1cp5RpDgsNeIWch5F8SHgC5ePET"
"eP/HkEd5lq24NltMMOcYhJL14CFsEMBL1LhhN5m1XV/20uDpDSK+soRY3l9hYkSlOkFuy1BCXm/aXOq4KR05Ear1qWzdP1Prh1/8AJo+f7"
"h9k/HGnDPuYF0DRte7Ajtt6+k1fP9TW8n7k+x+094/ea/40ZczOsQbFH39YEQ6+NAVmFRZC63iqltjLEY0iIsadJSBuWISs3LwNhNkaVGK"
"Rs+IgtDMCx0ZaaM17RbT1ZXGhTcsRob+c2RpUdqf4wBVe3lOxv0lNz27fuXRceX0mbTpt53KUY26dv3KUeXQ6RGx7Hb+uCBXn+Ihuir7fM"
"//Z"
)
base64_full = base64_header + base64_data
expected = ContentFile(base64.b64decode(base64_data), name='tmp.jpeg')
# assert that the data chunks are equal
self.assertListEqual(
list(StdImageSerializerField().to_internal_value(base64_full).chunks()),
list(expected.chunks())
)
@ddt.data("", False, None, [])
def test_to_internal_value_falsey(self, falsey_value):
self.assertIsNone(StdImageSerializerField().to_internal_value(falsey_value))
......@@ -644,6 +644,7 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
def get_expected_data(self, program, request):
expected = super().get_expected_data(program, request)
expected.update({
'authoring_organizations': OrganizationSerializer(program.authoring_organizations, many=True).data,
'video': VideoSerializer(program.video).data,
......@@ -655,8 +656,10 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
).data,
'expected_learning_items': [item.value for item in program.expected_learning_items.all()],
'faq': FAQSerializer(program.faq, many=True).data,
'individual_endorsements': EndorsementSerializer(program.individual_endorsements, many=True).data,
'staff': PersonSerializer(program.staff, many=True).data,
'individual_endorsements': EndorsementSerializer(
program.individual_endorsements, many=True, context={'request': request}
).data,
'staff': PersonSerializer(program.staff, many=True, context={'request': request}).data,
'job_outlook_items': [item.value for item in program.job_outlook_items.all()],
'languages': [serialize_language_to_code(l) for l in program.languages],
'weeks_to_complete': program.weeks_to_complete,
......@@ -1035,9 +1038,14 @@ class SeatSerializerTests(TestCase):
class PersonSerializerTests(TestCase):
def test_data(self):
request = make_request()
context = {'request': request}
image_field = StdImageSerializerField()
image_field._context = context # pylint: disable=protected-access
position = PositionFactory()
person = position.person
serializer = PersonSerializer(person)
serializer = PersonSerializer(person, context=context)
expected = {
'uuid': str(person.uuid),
......@@ -1045,6 +1053,7 @@ class PersonSerializerTests(TestCase):
'family_name': person.family_name,
'bio': person.bio,
'profile_image_url': person.profile_image_url,
'profile_image': image_field.to_representation(person.profile_image),
'position': PositionSerializer(position).data,
'slug': person.slug,
}
......
......@@ -8,7 +8,7 @@ from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseWithProgramsSerializer, CourseSerializerExcludingClosedRuns,
CourseRunWithProgramsSerializer, MinimalProgramSerializer, ProgramSerializer,
CourseRunWithProgramsSerializer, MinimalProgramSerializer, PersonSerializer, ProgramSerializer,
FlattenedCourseRunWithCourseSerializer, OrganizationSerializer, ProgramTypeSerializer
)
......@@ -41,6 +41,9 @@ class SerializationMixin(object):
def serialize_course_run(self, run, many=False, format=None, extra_context=None):
return self._serialize_object(CourseRunWithProgramsSerializer, run, many, format, extra_context)
def serialize_person(self, person, many=False, format=None, extra_context=None):
return self._serialize_object(PersonSerializer, person, many, format, extra_context)
def serialize_program(self, program, many=False, format=None, extra_context=None):
return self._serialize_object(
MinimalProgramSerializer if many else ProgramSerializer,
......
# pylint: disable=redefined-builtin,no-member
import json
import ddt
from django.conf import settings
from django.contrib.auth import get_user_model
from rest_framework.reverse import reverse
from rest_framework.test import APITestCase
from course_discovery.apps.api.v1.tests.test_views.mixins import SerializationMixin
from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.course_metadata.models import Person
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PartnerFactory, PersonFactory
User = get_user_model()
@ddt.ddt
class PersonViewSetTests(SerializationMixin, APITestCase):
""" Tests for the person resource. """
people_list_url = reverse('api:v1:person-list')
def setUp(self):
super(PersonViewSetTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.force_authenticate(self.user)
self.person = PersonFactory()
self.organization = OrganizationFactory()
# DEFAULT_PARTNER_ID is used explicitly here to avoid issues with differences in
# auto-incrementing behavior across databases. Otherwise, it's not safe to assume
# that the partner created here will always have id=DEFAULT_PARTNER_ID.
self.partner = PartnerFactory(id=settings.DEFAULT_PARTNER_ID)
def test_create_with_authentication(self):
""" Verify endpoint successfully creates a person. """
given_name = "Robert"
family_name = "Ford"
bio = "The maze is not for him."
title = "Park Director"
organization_id = self.organization.id
data = {
'data': json.dumps(
{
'given_name': given_name,
'family_name': family_name,
'bio': bio,
'position': {
'title': title,
'organization': organization_id
}
}
)
}
response = self.client.post(self.people_list_url, data, format='json')
self.assertEqual(response.status_code, 201)
person = Person.objects.last()
self.assertDictEqual(response.data, self.serialize_person(person))
self.assertEqual(person.given_name, given_name)
self.assertEqual(person.family_name, family_name)
self.assertEqual(person.bio, bio)
self.assertEqual(person.position.title, title)
self.assertEqual(person.position.organization, self.organization)
def test_create_without_authentication(self):
""" Verify authentication is required when creating a person. """
self.client.logout()
Person.objects.all().delete()
response = self.client.post(self.people_list_url, {}, format='json')
self.assertEqual(response.status_code, 403)
self.assertEqual(Person.objects.count(), 0)
def test_get(self):
""" Verify the endpoint returns the details for a single person. """
url = reverse('api:v1:person-detail', kwargs={'uuid': self.person.uuid})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data, self.serialize_person(self.person))
def test_list(self):
""" Verify the endpoint returns a list of all people. """
response = self.client.get(self.people_list_url)
self.assertEqual(response.status_code, 200)
self.assertListEqual(response.data['results'], self.serialize_person(Person.objects.all(), many=True))
......@@ -8,6 +8,7 @@ from course_discovery.apps.api.v1.views.catalogs import CatalogViewSet
from course_discovery.apps.api.v1.views.course_runs import CourseRunViewSet
from course_discovery.apps.api.v1.views.courses import CourseViewSet
from course_discovery.apps.api.v1.views.organizations import OrganizationViewSet
from course_discovery.apps.api.v1.views.people import PersonViewSet
from course_discovery.apps.api.v1.views.programs import ProgramViewSet, ProgramTypeListViewSet
partners_router = routers.SimpleRouter()
......@@ -23,6 +24,7 @@ router.register(r'catalogs', CatalogViewSet)
router.register(r'courses', CourseViewSet, base_name='course')
router.register(r'course_runs', CourseRunViewSet, base_name='course_run')
router.register(r'organizations', OrganizationViewSet, base_name='organization')
router.register(r'people', PersonViewSet, base_name='person')
router.register(r'programs', ProgramViewSet, base_name='program')
router.register(r'program_types', ProgramTypeListViewSet, base_name='program_type')
router.register(r'search/all', search_views.AggregateSearchViewSet, base_name='search-all')
......
import json
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from course_discovery.apps.api import serializers
from course_discovery.apps.api.pagination import PageNumberPagination
from course_discovery.apps.api.v1.views import PartnerMixin
# pylint: disable=no-member
class PersonViewSet(PartnerMixin, viewsets.ModelViewSet):
""" PersonSerializer resource. """
lookup_field = 'uuid'
lookup_value_regex = '[0-9a-f-]+'
permission_classes = (IsAuthenticated,)
queryset = serializers.PersonSerializer.prefetch_queryset()
serializer_class = serializers.PersonSerializer
pagination_class = PageNumberPagination
def create(self, request, *args, **kwargs):
""" Create a new person. """
person_data = json.loads(request.data.get('data'))
partner = self.get_partner()
person_data['partner'] = partner.id
serializer = self.get_serializer(data=person_data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def list(self, request, *args, **kwargs):
""" Retrieve a list of all people. """
return super(PersonViewSet, self).list(request, *args, **kwargs)
def retrieve(self, request, *args, **kwargs):
""" Retrieve details for a person. """
return super(PersonViewSet, self).retrieve(request, *args, **kwargs)
......@@ -66,9 +66,13 @@ class PersonAutocomplete(autocomplete.Select2QuerySetView):
return []
def get_result_label(self, result):
profile_image = result.profile_image_url
if hasattr(result.profile_image, 'url'):
profile_image = result.profile_image.url
context = {
'id': result.id,
'profile_image': result.profile_image_url,
'uuid': result.uuid,
'profile_image': profile_image,
'full_name': result.full_name,
'position': result.position if hasattr(result, 'position') else None
}
......
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2017-01-23 19:51
from __future__ import unicode_literals
from django.db import migrations
import stdimage.models
import stdimage.utils
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0044_auto_20170131_1749'),
]
operations = [
migrations.AddField(
model_name='person',
name='profile_image',
field=stdimage.models.StdImageField(blank=True, null=True, upload_to=stdimage.utils.UploadToAutoSlug('uuid', path='media/people/profile_images')),
),
]
......@@ -197,6 +197,14 @@ class Person(TimeStampedModel):
family_name = models.CharField(max_length=255, null=True, blank=True)
bio = models.TextField(null=True, blank=True)
profile_image_url = models.URLField(null=True, blank=True)
profile_image = StdImageField(
upload_to=UploadToAutoSlug(populate_from='uuid', path='media/people/profile_images'),
blank=True,
null=True,
variations={
'medium': (110, 110),
},
)
slug = AutoSlugField(populate_from=('given_name', 'family_name'), editable=True)
class Meta:
......
......@@ -172,6 +172,7 @@ class PersonFactory(factory.DjangoModelFactory):
family_name = factory.Faker('last_name')
bio = FuzzyText()
profile_image_url = FuzzyURL()
profile_image = FuzzyText(prefix='https://example.com/person/profile_image')
class Meta:
model = Person
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 15:35+0500\n"
"POT-Creation-Date: 2017-02-03 14:30-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 15:35+0500\n"
"POT-Creation-Date: 2017-02-03 14:30-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -28,6 +28,14 @@ msgstr ""
msgid "Show changes"
msgstr ""
#: static/js/publisher/publisher.js
msgid "Something went wrong!"
msgstr ""
#: static/js/publisher/publisher.js
msgid "File must be smaller than 1 megabyte in size."
msgstr ""
#: static/js/publisher/views/dashboard.js
msgid ""
"You have successfully created a studio instance ({studioLinkTag}) for "
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 15:35+0500\n"
"POT-Creation-Date: 2017-02-03 14:30-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......
......@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-02-03 15:35+0500\n"
"POT-Creation-Date: 2017-02-03 14:30-0500\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
......@@ -29,6 +29,16 @@ msgstr "Hïdé çhängés Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
msgid "Show changes"
msgstr "Shöw çhängés Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
#: static/js/publisher/publisher.js
msgid "Something went wrong!"
msgstr "Söméthïng wént wröng! Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, #"
#: static/js/publisher/publisher.js
msgid "File must be smaller than 1 megabyte in size."
msgstr ""
"Fïlé müst ßé smällér thän 1 mégäßýté ïn sïzé. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, "
"¢σηѕє¢тєтυя #"
#: static/js/publisher/views/dashboard.js
msgid ""
"You have successfully created a studio instance ({studioLinkTag}) for "
......
......@@ -58,6 +58,41 @@ $(document).ready(function(){
closeModal(e, $('#addInstructorModal'));
});
$('#add-instructor-btn').click(function (e) {
$.ajax({
type: "POST",
url: $(this).data('url'),
data: {
'data': JSON.stringify(
{
'given_name': $('#given-name').val(),
'family_name': $('#family-name').val(),
'bio': $('#bio').val(),
'profile_image': $('.select-image').attr('src'),
'position':{
'title': $('#title').val(),
'organization': parseInt($('#id_organization').val())
}
}
)
},
success: function (response) {
$('#given-name').val('');
$('#family-name').val('');
$('#title').val('');
$('#bio').val('');
$('.select-image').attr('src', '')
clearModalError();
closeModal(e, $('#addInstructorModal'));
},
error: function (response) {
addModalError(gettext("Something went wrong!"));
console.log(response);
}
});
});
$("#id_staff").find('option:selected').each(function(){
var id = this.value,
label = $.parseHTML(this.label),
......@@ -127,15 +162,23 @@ function loadAdminUsers(org_id) {
}
function loadSelectedImage(input) {
// 1mb in bytes
var maxFileSize = 1000000;
if (input.files && input.files[0]) {
if (input.files[0].size > maxFileSize) {
addModalError(gettext("File must be smaller than 1 megabyte in size."));
} else {
var reader = new FileReader();
clearModalError();
reader.onload = function (e) {
$('.select-image').attr('src', e.target.result);
};
reader.readAsDataURL(input.files[0]);
}
}
}
function closeModal(event, modal) {
......@@ -199,3 +242,16 @@ $(document).on('change', '#id_select_revisions', function (e) {
// on changing the revision from dropdown set the href of button.
$('#id_open_revision').prop("href", this.value);
});
function addModalError(errorMessage) {
var errorHtml = '<div class="alert alert-error" role="alert" aria-labelledby="alert-title-error" tabindex="-1">' +
'<div><p class="alert-copy">' + errorMessage + '</p></div></div>';
$('#modal-errors').html(errorHtml);
$('#modal-errors').show();
}
function clearModalError($modal) {
$('#modal-errors').html('');
$('#modal-errors').hide();
}
......@@ -3,6 +3,8 @@
<div id="addInstructorModal" class="modal">
<div class="modal-content">
<h2 class="hd-2 emphasized new-instructor-heading">{% trans "New Instructor" %}</h2>
<div id="modal-errors" class="alert-messages">
</div>
<form class="form">
<fieldset class="form-group">
<div class="staff-image-icon">
......@@ -53,7 +55,7 @@
</fieldset>
<div class="actions">
<a class="btn-cancel closeModal" href="#">{% trans "Cancel" %}</a>
<button class="btn-brand btn-base btn-save" type="button">{% trans "Add Staff Member" %}</button>
<button class="btn-brand btn-base btn-save" type="button" data-url="{% url 'api:v1:person-list' %}" id="add-instructor-btn">{% trans "Add Staff Member" %}</button>
</div>
</form>
</div>
......
<table class="instructor-option" id="{{ id }}">
<table class="instructor-option" id="{{ uuid }}">
<tr>
<td><img src="{{ profile_image }}" alt="profile image"/></td>
<td>
......
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