Commit 0716037d by Afzal Wali

Added Logo Image to the ProgramType

Created a new endpoing for ProgramType as a list-only viewset.
parent fbac0672
...@@ -689,6 +689,19 @@ class ProgramSerializer(MinimalProgramSerializer): ...@@ -689,6 +689,19 @@ class ProgramSerializer(MinimalProgramSerializer):
) )
class ProgramTypeSerializer(serializers.ModelSerializer):
"""
Serializer for the Program Types
"""
logo_image = StdImageSerializerField()
class Meta:
model = ProgramType
fields = (
'name', 'logo_image',
)
class AffiliateWindowSerializer(serializers.ModelSerializer): class AffiliateWindowSerializer(serializers.ModelSerializer):
""" Serializer for Affiliate Window product feeds. """ """ Serializer for Affiliate Window product feeds. """
......
...@@ -17,7 +17,7 @@ from course_discovery.apps.api.serializers import ( ...@@ -17,7 +17,7 @@ from course_discovery.apps.api.serializers import (
CourseRunWithProgramsSerializer, CourseWithProgramsSerializer, CorporateEndorsementSerializer, CourseRunWithProgramsSerializer, CourseWithProgramsSerializer, CorporateEndorsementSerializer,
FAQSerializer, EndorsementSerializer, PositionSerializer, FlattenedCourseRunWithCourseSerializer, FAQSerializer, EndorsementSerializer, PositionSerializer, FlattenedCourseRunWithCourseSerializer,
MinimalCourseSerializer, MinimalOrganizationSerializer, MinimalCourseRunSerializer, MinimalProgramSerializer, MinimalCourseSerializer, MinimalOrganizationSerializer, MinimalCourseRunSerializer, MinimalProgramSerializer,
CourseSerializer, TypeaheadCourseRunSearchSerializer, TypeaheadProgramSearchSerializer CourseSerializer, TypeaheadCourseRunSearchSerializer, TypeaheadProgramSearchSerializer, ProgramTypeSerializer
) )
from course_discovery.apps.catalogs.tests.factories import CatalogFactory from course_discovery.apps.catalogs.tests.factories import CatalogFactory
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
...@@ -28,7 +28,7 @@ from course_discovery.apps.course_metadata.models import CourseRun, Program ...@@ -28,7 +28,7 @@ from course_discovery.apps.course_metadata.models import CourseRun, Program
from course_discovery.apps.course_metadata.tests.factories import ( from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, SubjectFactory, PrerequisiteFactory, ImageFactory, VideoFactory, CourseFactory, CourseRunFactory, SubjectFactory, PrerequisiteFactory, ImageFactory, VideoFactory,
OrganizationFactory, PersonFactory, SeatFactory, ProgramFactory, CorporateEndorsementFactory, EndorsementFactory, OrganizationFactory, PersonFactory, SeatFactory, ProgramFactory, CorporateEndorsementFactory, EndorsementFactory,
JobOutlookItemFactory, ExpectedLearningItemFactory, PositionFactory JobOutlookItemFactory, ExpectedLearningItemFactory, PositionFactory, ProgramTypeFactory
) )
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
...@@ -764,6 +764,26 @@ class ProgramSerializerTests(MinimalProgramSerializerTests): ...@@ -764,6 +764,26 @@ class ProgramSerializerTests(MinimalProgramSerializerTests):
self.assertDictEqual(serializer.data, expected) self.assertDictEqual(serializer.data, expected)
class ProgramTypeSerializerTests(TestCase):
serializer_class = ProgramTypeSerializer
def get_expected_data(self, program_type, request):
image_field = StdImageSerializerField()
image_field._context = {'request': request} # pylint: disable=protected-access
return {
'name': program_type.name,
'logo_image': image_field.to_representation(program_type.logo_image),
}
def test_data(self):
request = make_request()
program_type = ProgramTypeFactory()
serializer = self.serializer_class(program_type, context={'request': request})
expected = self.get_expected_data(program_type, request)
self.assertDictEqual(serializer.data, expected)
class ContainedCourseRunsSerializerTests(TestCase): class ContainedCourseRunsSerializerTests(TestCase):
def test_data(self): def test_data(self):
instance = { instance = {
......
...@@ -9,7 +9,7 @@ from rest_framework.test import APIRequestFactory ...@@ -9,7 +9,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, ProgramSerializer,
FlattenedCourseRunWithCourseSerializer, OrganizationSerializer FlattenedCourseRunWithCourseSerializer, OrganizationSerializer, ProgramTypeSerializer
) )
...@@ -50,6 +50,9 @@ class SerializationMixin(object): ...@@ -50,6 +50,9 @@ class SerializationMixin(object):
extra_context extra_context
) )
def serialize_program_type(self, program_type, many=False, format=None, extra_context=None):
return self._serialize_object(ProgramTypeSerializer, program_type, many, format, extra_context)
def serialize_catalog_course(self, course, many=False, format=None, extra_context=None): def serialize_catalog_course(self, course, many=False, format=None, extra_context=None):
return self._serialize_object(CourseSerializerExcludingClosedRuns, course, many, format, extra_context) return self._serialize_object(CourseSerializerExcludingClosedRuns, course, many, format, extra_context)
......
...@@ -8,10 +8,11 @@ from course_discovery.apps.api.v1.views.programs import ProgramViewSet ...@@ -8,10 +8,11 @@ from course_discovery.apps.api.v1.views.programs import ProgramViewSet
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.core.tests.helpers import make_image_file from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.choices import ProgramStatus from course_discovery.apps.course_metadata.choices import ProgramStatus
from course_discovery.apps.course_metadata.models import Program from course_discovery.apps.course_metadata.models import Program, ProgramType
from course_discovery.apps.course_metadata.tests.factories import ( from course_discovery.apps.course_metadata.tests.factories import (
CourseFactory, CourseRunFactory, VideoFactory, OrganizationFactory, PersonFactory, ProgramFactory, CourseFactory, CourseRunFactory, VideoFactory, OrganizationFactory, PersonFactory, ProgramFactory,
CorporateEndorsementFactory, EndorsementFactory, JobOutlookItemFactory, ExpectedLearningItemFactory CorporateEndorsementFactory, EndorsementFactory, JobOutlookItemFactory, ExpectedLearningItemFactory,
ProgramTypeFactory
) )
...@@ -160,3 +161,49 @@ class ProgramViewSetTests(SerializationMixin, APITestCase): ...@@ -160,3 +161,49 @@ class ProgramViewSetTests(SerializationMixin, APITestCase):
def test_minimal_serializer_use(self): def test_minimal_serializer_use(self):
""" Verify that the list view uses the minimal serializer. """ """ Verify that the list view uses the minimal serializer. """
self.assertEqual(ProgramViewSet(action='list').get_serializer_class(), MinimalProgramSerializer) self.assertEqual(ProgramViewSet(action='list').get_serializer_class(), MinimalProgramSerializer)
class ProgramTypeViewSetTests(SerializationMixin, APITestCase):
list_path = reverse('api:v1:program_type-list')
def setUp(self):
super(ProgramTypeViewSetTests, self).setUp()
self.user = UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=self.user.username, password=USER_PASSWORD)
def test_authentication(self):
""" Verify the endpoint requires the user to be authenticated. """
response = self.client.get(self.list_path)
self.assertEqual(response.status_code, 200)
self.client.logout()
response = self.client.get(self.list_path)
self.assertEqual(response.status_code, 403)
def assert_list_results(self, url, expected, expected_query_count, extra_context=None):
"""
Asserts the results serialized/returned at the URL matches those that are expected.
Args:
url (str): URL from which data should be retrieved
expected (list[ProgramType]): Expected program_types
Notes:
The API usually returns items in reverse order of creation (e.g. newest first). You may need to reverse
the values of `expected` if you encounter issues. This method will NOT do that reversal for you.
Returns:
None
"""
with self.assertNumQueries(expected_query_count):
response = self.client.get(url)
self.assertEqual(
response.data['results'],
self.serialize_program_type(expected, many=True, extra_context=extra_context)
)
def test_list(self):
""" Verify the endpoint returns a list of all program_types. """
ProgramTypeFactory.create_batch(3)
expected = ProgramType.objects.all()
self.assert_list_results(self.list_path, expected, 4)
...@@ -8,7 +8,7 @@ from course_discovery.apps.api.v1.views.catalogs import CatalogViewSet ...@@ -8,7 +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.programs import ProgramViewSet from course_discovery.apps.api.v1.views.programs import ProgramViewSet, ProgramTypeListViewSet
partners_router = routers.SimpleRouter() partners_router = routers.SimpleRouter()
partners_router.register(r'affiliate_window/catalogs', AffiliateWindowViewSet, base_name='affiliate_window') partners_router.register(r'affiliate_window/catalogs', AffiliateWindowViewSet, base_name='affiliate_window')
...@@ -24,6 +24,7 @@ router.register(r'courses', CourseViewSet, base_name='course') ...@@ -24,6 +24,7 @@ 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'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'search/all', search_views.AggregateSearchViewSet, base_name='search-all') router.register(r'search/all', search_views.AggregateSearchViewSet, base_name='search-all')
router.register(r'search/courses', search_views.CourseSearchViewSet, base_name='search-courses') router.register(r'search/courses', search_views.CourseSearchViewSet, base_name='search-courses')
router.register(r'search/course_runs', search_views.CourseRunSearchViewSet, base_name='search-course_runs') router.register(r'search/course_runs', search_views.CourseRunSearchViewSet, base_name='search-course_runs')
......
from rest_framework import viewsets from rest_framework import mixins, viewsets
from rest_framework.filters import DjangoFilterBackend from rest_framework.filters import DjangoFilterBackend
from rest_framework.permissions import IsAuthenticated from rest_framework.permissions import IsAuthenticated
from course_discovery.apps.api import filters, serializers from course_discovery.apps.api import filters, serializers
from course_discovery.apps.api.v1.views import get_query_param from course_discovery.apps.api.v1.views import get_query_param
from course_discovery.apps.course_metadata.models import ProgramType
# pylint: disable=no-member # pylint: disable=no-member
...@@ -66,3 +67,11 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -66,3 +67,11 @@ class ProgramViewSet(viewsets.ReadOnlyModelViewSet):
multiple: false multiple: false
""" """
return super(ProgramViewSet, self).list(request, *args, **kwargs) return super(ProgramViewSet, self).list(request, *args, **kwargs)
class ProgramTypeListViewSet(mixins.ListModelMixin,
viewsets.GenericViewSet):
""" ProgramType resource. """
serializer_class = serializers.ProgramTypeSerializer
permission_classes = (IsAuthenticated,)
queryset = ProgramType.objects.all()
# -*- coding: utf-8 -*-
# Generated by Django 1.9.11 on 2016-12-13 10:57
from __future__ import unicode_literals
from django.db import migrations
import stdimage.models
import stdimage.utils
class Migration(migrations.Migration):
dependencies = [
('course_metadata', '0038_seat_sku'),
]
operations = [
migrations.AddField(
model_name='programtype',
name='logo_image',
field=stdimage.models.StdImageField(blank=True, help_text='Please provide an image file with transparent background', null=True, upload_to=stdimage.utils.UploadToAutoSlug('name', path='media/program_types/logo_images')),
),
]
...@@ -17,6 +17,7 @@ from haystack import connections ...@@ -17,6 +17,7 @@ from haystack import connections
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from sortedm2m.fields import SortedManyToManyField from sortedm2m.fields import SortedManyToManyField
from stdimage.models import StdImageField from stdimage.models import StdImageField
from stdimage.utils import UploadToAutoSlug
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from course_discovery.apps.core.models import Currency, Partner from course_discovery.apps.core.models import Currency, Partner
...@@ -580,6 +581,18 @@ class ProgramType(TimeStampedModel): ...@@ -580,6 +581,18 @@ class ProgramType(TimeStampedModel):
'associated courses, but enrolled in other seat types, will NOT have their completion ' 'associated courses, but enrolled in other seat types, will NOT have their completion '
'of the course counted toward the completion of the program.'), 'of the course counted toward the completion of the program.'),
) )
logo_image = StdImageField(
upload_to=UploadToAutoSlug(populate_from='name', path='media/program_types/logo_images'),
blank=True,
null=True,
variations={
'large': (256, 256),
'medium': (128, 128),
'small': (64, 64),
'x-small': (32, 32),
},
help_text=_('Please provide an image file with transparent background'),
)
def __str__(self): def __str__(self):
return self.name return self.name
......
...@@ -189,6 +189,7 @@ class ProgramTypeFactory(factory.django.DjangoModelFactory): ...@@ -189,6 +189,7 @@ class ProgramTypeFactory(factory.django.DjangoModelFactory):
model = ProgramType model = ProgramType
name = FuzzyText() name = FuzzyText()
logo_image = FuzzyText(prefix='https://example.com/program/logo')
@factory.post_generation @factory.post_generation
def applicable_seat_types(self, create, extracted, **kwargs): def applicable_seat_types(self, create, extracted, **kwargs):
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2016-12-08 15:03+0500\n" "POT-Creation-Date: 2016-12-14 15:06+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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: apps/api/filters.py #: apps/api/filters.py
#, python-brace-format #, python-brace-format
...@@ -362,6 +362,10 @@ msgid "" ...@@ -362,6 +362,10 @@ msgid ""
msgstr "" msgstr ""
#: apps/course_metadata/models.py #: apps/course_metadata/models.py
msgid "Please provide an image file with transparent background"
msgstr ""
#: apps/course_metadata/models.py
msgid "The user-facing display title for this Program." msgid "The user-facing display title for this Program."
msgstr "" msgstr ""
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2016-12-08 15:03+0500\n" "POT-Creation-Date: 2016-12-14 15:06+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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
msgid "Preview" msgid "Preview"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2016-12-08 15:03+0500\n" "POT-Creation-Date: 2016-12-14 15:06+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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: apps/api/filters.py #: apps/api/filters.py
...@@ -449,6 +449,12 @@ msgstr "" ...@@ -449,6 +449,12 @@ msgstr ""
" єѕѕє ¢ιłłυм ∂σłσяє є#" " єѕѕє ¢ιłłυм ∂σłσяє є#"
#: apps/course_metadata/models.py #: apps/course_metadata/models.py
msgid "Please provide an image file with transparent background"
msgstr ""
"Pléäsé prövïdé än ïmägé fïlé wïth tränspärént ßäçkgröünd Ⱡ'σяєм ιρѕυм ∂σłσя "
"ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: apps/course_metadata/models.py
msgid "The user-facing display title for this Program." msgid "The user-facing display title for this Program."
msgstr "" msgstr ""
"Thé üsér-fäçïng dïspläý tïtlé för thïs Prögräm. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт," "Thé üsér-fäçïng dïspläý tïtlé för thïs Prögräm. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт,"
......
...@@ -7,14 +7,14 @@ msgid "" ...@@ -7,14 +7,14 @@ 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: 2016-12-08 15:03+0500\n" "POT-Creation-Date: 2016-12-14 15:06+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"
"Language: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: static/js/catalogs-change-form.js #: static/js/catalogs-change-form.js
......
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