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 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 Custom serializer field to render out proper JSON representation of the StdImage field on model
""" """
...@@ -21,9 +24,19 @@ class StdImageSerializerField(serializers.Field): ...@@ -21,9 +24,19 @@ class StdImageSerializerField(serializers.Field):
return serialized return serialized
def to_internal_value(self, obj): def to_internal_value(self, data):
""" We do not need to save/edit this banner image through serializer yet """ """ Save base 64 encoded images """
pass # 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 class ImageField(serializers.Field): # pylint:disable=abstract-method
......
...@@ -201,12 +201,16 @@ class PositionSerializer(serializers.ModelSerializer): ...@@ -201,12 +201,16 @@ class PositionSerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
model = Position model = Position
fields = ('title', 'organization_name',) fields = ('title', 'organization_name', 'organization')
extra_kwargs = {
'organization': {'write_only': True}
}
class PersonSerializer(serializers.ModelSerializer): class PersonSerializer(serializers.ModelSerializer):
"""Serializer for the ``Person`` model.""" """Serializer for the ``Person`` model."""
position = PositionSerializer() position = PositionSerializer(required=False)
profile_image = StdImageSerializerField(required=False)
@classmethod @classmethod
def prefetch_queryset(cls): def prefetch_queryset(cls):
...@@ -214,7 +218,19 @@ class PersonSerializer(serializers.ModelSerializer): ...@@ -214,7 +218,19 @@ class PersonSerializer(serializers.ModelSerializer):
class Meta(object): class Meta(object):
model = Person 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): 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 django.test import TestCase
from course_discovery.apps.api.fields import ImageField, StdImageSerializerField from course_discovery.apps.api.fields import ImageField, StdImageSerializerField
...@@ -18,7 +23,7 @@ class ImageFieldTests(TestCase): ...@@ -18,7 +23,7 @@ class ImageFieldTests(TestCase):
self.assertEqual(ImageField().to_representation(value), expected) self.assertEqual(ImageField().to_representation(value), expected)
# pylint: disable=no-member @ddt.ddt
class StdImageSerializerFieldTests(TestCase): class StdImageSerializerFieldTests(TestCase):
def test_to_representation(self): def test_to_representation(self):
request = make_request() request = make_request()
...@@ -40,3 +45,46 @@ class StdImageSerializerFieldTests(TestCase): ...@@ -40,3 +45,46 @@ class StdImageSerializerFieldTests(TestCase):
} }
self.assertDictEqual(field.to_representation(program.banner_image), expected) 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): ...@@ -644,6 +644,7 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
def get_expected_data(self, program, request): def get_expected_data(self, program, request):
expected = super().get_expected_data(program, request) expected = super().get_expected_data(program, request)
expected.update({ expected.update({
'authoring_organizations': OrganizationSerializer(program.authoring_organizations, many=True).data, 'authoring_organizations': OrganizationSerializer(program.authoring_organizations, many=True).data,
'video': VideoSerializer(program.video).data, 'video': VideoSerializer(program.video).data,
...@@ -655,8 +656,10 @@ class ProgramSerializerTests(MinimalProgramSerializerTests): ...@@ -655,8 +656,10 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
).data, ).data,
'expected_learning_items': [item.value for item in program.expected_learning_items.all()], 'expected_learning_items': [item.value for item in program.expected_learning_items.all()],
'faq': FAQSerializer(program.faq, many=True).data, 'faq': FAQSerializer(program.faq, many=True).data,
'individual_endorsements': EndorsementSerializer(program.individual_endorsements, many=True).data, 'individual_endorsements': EndorsementSerializer(
'staff': PersonSerializer(program.staff, many=True).data, 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()], 'job_outlook_items': [item.value for item in program.job_outlook_items.all()],
'languages': [serialize_language_to_code(l) for l in program.languages], 'languages': [serialize_language_to_code(l) for l in program.languages],
'weeks_to_complete': program.weeks_to_complete, 'weeks_to_complete': program.weeks_to_complete,
...@@ -1035,9 +1038,14 @@ class SeatSerializerTests(TestCase): ...@@ -1035,9 +1038,14 @@ class SeatSerializerTests(TestCase):
class PersonSerializerTests(TestCase): class PersonSerializerTests(TestCase):
def test_data(self): def test_data(self):
request = make_request()
context = {'request': request}
image_field = StdImageSerializerField()
image_field._context = context # pylint: disable=protected-access
position = PositionFactory() position = PositionFactory()
person = position.person person = position.person
serializer = PersonSerializer(person) serializer = PersonSerializer(person, context=context)
expected = { expected = {
'uuid': str(person.uuid), 'uuid': str(person.uuid),
...@@ -1045,6 +1053,7 @@ class PersonSerializerTests(TestCase): ...@@ -1045,6 +1053,7 @@ class PersonSerializerTests(TestCase):
'family_name': person.family_name, 'family_name': person.family_name,
'bio': person.bio, 'bio': person.bio,
'profile_image_url': person.profile_image_url, 'profile_image_url': person.profile_image_url,
'profile_image': image_field.to_representation(person.profile_image),
'position': PositionSerializer(position).data, 'position': PositionSerializer(position).data,
'slug': person.slug, 'slug': person.slug,
} }
......
...@@ -8,7 +8,7 @@ from rest_framework.test import APIRequestFactory ...@@ -8,7 +8,7 @@ from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import ( from course_discovery.apps.api.serializers import (
CatalogSerializer, CourseWithProgramsSerializer, CourseSerializerExcludingClosedRuns, CatalogSerializer, CourseWithProgramsSerializer, CourseSerializerExcludingClosedRuns,
CourseRunWithProgramsSerializer, MinimalProgramSerializer, ProgramSerializer, CourseRunWithProgramsSerializer, MinimalProgramSerializer, PersonSerializer, ProgramSerializer,
FlattenedCourseRunWithCourseSerializer, OrganizationSerializer, ProgramTypeSerializer FlattenedCourseRunWithCourseSerializer, OrganizationSerializer, ProgramTypeSerializer
) )
...@@ -41,6 +41,9 @@ class SerializationMixin(object): ...@@ -41,6 +41,9 @@ class SerializationMixin(object):
def serialize_course_run(self, run, many=False, format=None, extra_context=None): def serialize_course_run(self, run, many=False, format=None, extra_context=None):
return self._serialize_object(CourseRunWithProgramsSerializer, run, many, format, extra_context) 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): def serialize_program(self, program, many=False, format=None, extra_context=None):
return self._serialize_object( return self._serialize_object(
MinimalProgramSerializer if many else ProgramSerializer, 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 ...@@ -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.course_runs import CourseRunViewSet
from course_discovery.apps.api.v1.views.courses import CourseViewSet 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.organizations import OrganizationViewSet
from course_discovery.apps.api.v1.views.people import PersonViewSet
from course_discovery.apps.api.v1.views.programs import ProgramViewSet, ProgramTypeListViewSet from course_discovery.apps.api.v1.views.programs import ProgramViewSet, ProgramTypeListViewSet
partners_router = routers.SimpleRouter() partners_router = routers.SimpleRouter()
...@@ -23,6 +24,7 @@ router.register(r'catalogs', CatalogViewSet) ...@@ -23,6 +24,7 @@ router.register(r'catalogs', CatalogViewSet)
router.register(r'courses', CourseViewSet, base_name='course') router.register(r'courses', CourseViewSet, base_name='course')
router.register(r'course_runs', CourseRunViewSet, base_name='course_run') router.register(r'course_runs', CourseRunViewSet, base_name='course_run')
router.register(r'organizations', OrganizationViewSet, base_name='organization') 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'programs', ProgramViewSet, base_name='program')
router.register(r'program_types', ProgramTypeListViewSet, base_name='program_type') router.register(r'program_types', ProgramTypeListViewSet, base_name='program_type')
router.register(r'search/all', search_views.AggregateSearchViewSet, base_name='search-all') 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): ...@@ -66,9 +66,13 @@ class PersonAutocomplete(autocomplete.Select2QuerySetView):
return [] return []
def get_result_label(self, result): 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 = { context = {
'id': result.id, 'uuid': result.uuid,
'profile_image': result.profile_image_url, 'profile_image': profile_image,
'full_name': result.full_name, 'full_name': result.full_name,
'position': result.position if hasattr(result, 'position') else None '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): ...@@ -197,6 +197,14 @@ class Person(TimeStampedModel):
family_name = models.CharField(max_length=255, null=True, blank=True) family_name = models.CharField(max_length=255, null=True, blank=True)
bio = models.TextField(null=True, blank=True) bio = models.TextField(null=True, blank=True)
profile_image_url = models.URLField(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) slug = AutoSlugField(populate_from=('given_name', 'family_name'), editable=True)
class Meta: class Meta:
......
...@@ -172,6 +172,7 @@ class PersonFactory(factory.DjangoModelFactory): ...@@ -172,6 +172,7 @@ class PersonFactory(factory.DjangoModelFactory):
family_name = factory.Faker('last_name') family_name = factory.Faker('last_name')
bio = FuzzyText() bio = FuzzyText()
profile_image_url = FuzzyURL() profile_image_url = FuzzyURL()
profile_image = FuzzyText(prefix='https://example.com/person/profile_image')
class Meta: class Meta:
model = Person model = Person
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -28,6 +28,14 @@ msgstr "" ...@@ -28,6 +28,14 @@ msgstr ""
msgid "Show changes" msgid "Show changes"
msgstr "" 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 #: static/js/publisher/views/dashboard.js
msgid "" msgid ""
"You have successfully created a studio instance ({studioLinkTag}) for " "You have successfully created a studio instance ({studioLinkTag}) for "
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
......
...@@ -7,7 +7,7 @@ msgid "" ...@@ -7,7 +7,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \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" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
...@@ -29,6 +29,16 @@ msgstr "Hïdé çhängés Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" ...@@ -29,6 +29,16 @@ msgstr "Hïdé çhängés Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#"
msgid "Show changes" msgid "Show changes"
msgstr "Shöw çhängés Ⱡ'σяєм ιρѕυм ∂σłσя ѕ#" 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 #: static/js/publisher/views/dashboard.js
msgid "" msgid ""
"You have successfully created a studio instance ({studioLinkTag}) for " "You have successfully created a studio instance ({studioLinkTag}) for "
......
...@@ -58,6 +58,41 @@ $(document).ready(function(){ ...@@ -58,6 +58,41 @@ $(document).ready(function(){
closeModal(e, $('#addInstructorModal')); 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(){ $("#id_staff").find('option:selected').each(function(){
var id = this.value, var id = this.value,
label = $.parseHTML(this.label), label = $.parseHTML(this.label),
...@@ -127,14 +162,22 @@ function loadAdminUsers(org_id) { ...@@ -127,14 +162,22 @@ function loadAdminUsers(org_id) {
} }
function loadSelectedImage(input) { function loadSelectedImage(input) {
// 1mb in bytes
var maxFileSize = 1000000;
if (input.files && input.files[0]) { if (input.files && input.files[0]) {
var reader = new FileReader(); if (input.files[0].size > maxFileSize) {
addModalError(gettext("File must be smaller than 1 megabyte in size."));
} else {
var reader = new FileReader();
reader.onload = function (e) { clearModalError();
$('.select-image').attr('src', e.target.result); reader.onload = function (e) {
}; $('.select-image').attr('src', e.target.result);
};
reader.readAsDataURL(input.files[0]); reader.readAsDataURL(input.files[0]);
}
} }
} }
...@@ -199,3 +242,16 @@ $(document).on('change', '#id_select_revisions', function (e) { ...@@ -199,3 +242,16 @@ $(document).on('change', '#id_select_revisions', function (e) {
// on changing the revision from dropdown set the href of button. // on changing the revision from dropdown set the href of button.
$('#id_open_revision').prop("href", this.value); $('#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 @@ ...@@ -3,6 +3,8 @@
<div id="addInstructorModal" class="modal"> <div id="addInstructorModal" class="modal">
<div class="modal-content"> <div class="modal-content">
<h2 class="hd-2 emphasized new-instructor-heading">{% trans "New Instructor" %}</h2> <h2 class="hd-2 emphasized new-instructor-heading">{% trans "New Instructor" %}</h2>
<div id="modal-errors" class="alert-messages">
</div>
<form class="form"> <form class="form">
<fieldset class="form-group"> <fieldset class="form-group">
<div class="staff-image-icon"> <div class="staff-image-icon">
...@@ -53,7 +55,7 @@ ...@@ -53,7 +55,7 @@
</fieldset> </fieldset>
<div class="actions"> <div class="actions">
<a class="btn-cancel closeModal" href="#">{% trans "Cancel" %}</a> <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> </div>
</form> </form>
</div> </div>
......
<table class="instructor-option" id="{{ id }}"> <table class="instructor-option" id="{{ uuid }}">
<tr> <tr>
<td><img src="{{ profile_image }}" alt="profile image"/></td> <td><img src="{{ profile_image }}" alt="profile image"/></td>
<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