Commit 9a33d0bf by Uman Shahzad Committed by Uman Shahzad

Add full detail search endpoints.

* Ignore all types of coverage files.

  I was getting things like `.coverage.$user`.

* Refactor existing serializers.

  Some serializers had their metadata all the way at the top
  of the file in separate variables, even though no other
  serializer were using those variables.

* Put things into proper places to make it easier to read
  and change, without having to jump up and down a 1000+
  line file.

* Add new serializers and use them in viewsets.

  These are the serializers for all 4 new endpoints,
  for courses, course runs, programs, and their aggregation.

* Add new mixin for detail endpoint.

  The mixin will add `/detail` to the end of whatever viewset
  that uses it.

* Call isort on the codebase.

* Refactor serializer test code.

  This will make it x10 easier to add tests for the
  serializers we introduced in this PR.

* Add test code for serializers.

* Refactor and add tests for viewsets.
parent 31ac8f5c
...@@ -33,6 +33,7 @@ assets/ ...@@ -33,6 +33,7 @@ assets/
# Unit test / coverage reports # Unit test / coverage reports
.coverage .coverage
.coverage.*
htmlcov htmlcov
.tox .tox
nosetests.xml nosetests.xml
......
from rest_framework import status
from rest_framework.exceptions import APIException
class InvalidPartnerError(APIException):
status_code = status.HTTP_400_BAD_REQUEST
"""
Mixins for the API application.
"""
# pylint: disable=not-callable
from rest_framework.decorators import list_route
from rest_framework.response import Response
class DetailMixin(object):
"""Mixin for adding in a detail endpoint using a special detail serializer."""
detail_serializer_class = None
@list_route(methods=['get'])
def details(self, request): # pylint: disable=unused-argument
"""
List detailed results.
---
parameters:
- name: q
description: Search text
paramType: query
type: string
required: false
"""
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_detail_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_detail_serializer(queryset, many=True)
return Response(serializer.data)
def get_detail_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_detail_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
def get_detail_serializer_class(self):
"""
Return the class to use for the serializer.
Defaults to using `self.detail_serializer_class`.
"""
assert self.detail_serializer_class is not None, (
"'%s' should either include a `detail_serializer_class` attribute, "
"or override the `get_detail_serializer_class()` method."
% self.__class__.__name__
)
return self.detail_serializer_class
import hashlib import hashlib
import logging import logging
import six import six
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -4,16 +4,15 @@ import json ...@@ -4,16 +4,15 @@ import json
import responses import responses
from django.conf import settings from django.conf import settings
from haystack.query import SearchQuerySet
from rest_framework.test import APITestCase as RestAPITestCase from rest_framework.test import APITestCase as RestAPITestCase
from rest_framework.test import APIRequestFactory from rest_framework.test import APIRequestFactory
from course_discovery.apps.api.serializers import ( from course_discovery.apps.api import serializers
CatalogCourseSerializer, CatalogSerializer, CourseRunWithProgramsSerializer,
CourseWithProgramsSerializer, FlattenedCourseRunWithCourseSerializer, MinimalProgramSerializer,
OrganizationSerializer, PersonSerializer, ProgramSerializer, ProgramTypeSerializer, SubjectSerializer,
TopicSerializer
)
from course_discovery.apps.api.tests.mixins import SiteMixin from course_discovery.apps.api.tests.mixins import SiteMixin
from course_discovery.apps.core.tests.factories import USER_PASSWORD, UserFactory
from course_discovery.apps.course_metadata.models import CourseRun, Program
from course_discovery.apps.course_metadata.tests import factories
class SerializationMixin: class SerializationMixin:
...@@ -32,47 +31,69 @@ class SerializationMixin: ...@@ -32,47 +31,69 @@ class SerializationMixin:
context = {'request': self._get_request(format)} context = {'request': self._get_request(format)}
if extra_context: if extra_context:
context.update(extra_context) context.update(extra_context)
return serializer(obj, many=many, context=context).data return serializer(obj, many=many, context=context).data
def _get_search_result(self, model, **kwargs):
return SearchQuerySet().models(model).filter(**kwargs)[0]
def serialize_catalog(self, catalog, many=False, format=None, extra_context=None): def serialize_catalog(self, catalog, many=False, format=None, extra_context=None):
return self._serialize_object(CatalogSerializer, catalog, many, format, extra_context) return self._serialize_object(serializers.CatalogSerializer, catalog, many, format, extra_context)
def serialize_course(self, course, many=False, format=None, extra_context=None): def serialize_course(self, course, many=False, format=None, extra_context=None):
return self._serialize_object(CourseWithProgramsSerializer, course, many, format, extra_context) return self._serialize_object(serializers.CourseWithProgramsSerializer, course, many, format, extra_context)
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(serializers.CourseRunWithProgramsSerializer, run, many, format, extra_context)
def serialize_course_run_search(self, run, serializer=None):
obj = self._get_search_result(CourseRun, key=run.key)
return self._serialize_object(serializer or serializers.CourseRunSearchSerializer, obj)
def serialize_person(self, person, many=False, format=None, extra_context=None): def serialize_person(self, person, many=False, format=None, extra_context=None):
return self._serialize_object(PersonSerializer, person, many, format, extra_context) return self._serialize_object(serializers.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, serializers.MinimalProgramSerializer if many else serializers.ProgramSerializer,
program, program,
many, many,
format, format,
extra_context extra_context
) )
def serialize_program_search(self, program, serializer=None):
obj = self._get_search_result(Program, uuid=program.uuid)
return self._serialize_object(serializer or serializers.ProgramSearchSerializer, obj)
def serialize_program_type(self, program_type, many=False, format=None, extra_context=None): 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) return self._serialize_object(serializers.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(CatalogCourseSerializer, course, many, format, extra_context) return self._serialize_object(serializers.CatalogCourseSerializer, course, many, format, extra_context)
def serialize_catalog_flat_course_run(self, course_run, many=False, format=None, extra_context=None): def serialize_catalog_flat_course_run(self, course_run, many=False, format=None, extra_context=None):
return self._serialize_object(FlattenedCourseRunWithCourseSerializer, course_run, many, format, extra_context) return self._serialize_object(
serializers.FlattenedCourseRunWithCourseSerializer, course_run, many, format, extra_context
)
def serialize_organization(self, organization, many=False, format=None, extra_context=None): def serialize_organization(self, organization, many=False, format=None, extra_context=None):
return self._serialize_object(OrganizationSerializer, organization, many, format, extra_context) return self._serialize_object(serializers.OrganizationSerializer, organization, many, format, extra_context)
def serialize_subject(self, subject, many=False, format=None, extra_context=None): def serialize_subject(self, subject, many=False, format=None, extra_context=None):
return self._serialize_object(SubjectSerializer, subject, many, format, extra_context) return self._serialize_object(serializers.SubjectSerializer, subject, many, format, extra_context)
def serialize_topic(self, topic, many=False, format=None, extra_context=None): def serialize_topic(self, topic, many=False, format=None, extra_context=None):
return self._serialize_object(TopicSerializer, topic, many, format, extra_context) return self._serialize_object(serializers.TopicSerializer, topic, many, format, extra_context)
class TypeaheadSerializationMixin:
def serialize_course_run_search(self, run):
obj = SearchQuerySet().models(CourseRun).filter(key=run.key)[0]
return serializers.TypeaheadCourseRunSearchSerializer(obj).data
def serialize_program_search(self, program):
obj = SearchQuerySet().models(Program).filter(uuid=program.uuid)[0]
return serializers.TypeaheadProgramSearchSerializer(obj).data
class OAuth2Mixin(object): class OAuth2Mixin(object):
...@@ -99,5 +120,54 @@ class OAuth2Mixin(object): ...@@ -99,5 +120,54 @@ class OAuth2Mixin(object):
) )
class SynonymTestMixin:
def test_org_synonyms(self):
""" Test that synonyms work for organization names """
title = 'UniversityX'
authoring_organizations = [factories.OrganizationFactory(name='University')]
factories.CourseRunFactory(
title=title,
course__partner=self.partner,
authoring_organizations=authoring_organizations
)
factories.ProgramFactory(title=title, partner=self.partner, authoring_organizations=authoring_organizations)
response1 = self.process_response({'q': title})
response2 = self.process_response({'q': 'University'})
assert response1 == response2
def test_title_synonyms(self):
""" Test that synonyms work for terms in the title """
factories.CourseRunFactory(title='HTML', course__partner=self.partner)
factories.ProgramFactory(title='HTML', partner=self.partner)
response1 = self.process_response({'q': 'HTML5'})
response2 = self.process_response({'q': 'HTML'})
assert response1 == response2
def test_special_character_synonyms(self):
""" Test that synonyms work with special characters (non ascii) """
factories.ProgramFactory(title='spanish', partner=self.partner)
response1 = self.process_response({'q': 'spanish'})
response2 = self.process_response({'q': 'español'})
assert response1 == response2
def test_stemmed_synonyms(self):
""" Test that synonyms work with stemming from the snowball analyzer """
title = 'Running'
factories.ProgramFactory(title=title, partner=self.partner)
response1 = self.process_response({'q': 'running'})
response2 = self.process_response({'q': 'jogging'})
assert response1 == response2
class LoginMixin:
def setUp(self):
super(LoginMixin, self).setUp()
self.user = UserFactory()
self.client.login(username=self.user.username, password=USER_PASSWORD)
if getattr(self, 'request'):
self.request.user = self.user
class APITestCase(SiteMixin, RestAPITestCase): class APITestCase(SiteMixin, RestAPITestCase):
pass pass
...@@ -2,7 +2,6 @@ import datetime ...@@ -2,7 +2,6 @@ import datetime
import ddt import ddt
import pytz import pytz
from django.core.cache import cache from django.core.cache import cache
from django.db.models.functions import Lower from django.db.models.functions import Lower
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
......
...@@ -115,7 +115,7 @@ class TestProgramViewSet(SerializationMixin): ...@@ -115,7 +115,7 @@ class TestProgramViewSet(SerializationMixin):
partner=self.partner) partner=self.partner)
# property does not have the right values while being indexed # property does not have the right values while being indexed
del program._course_run_weeks_to_complete del program._course_run_weeks_to_complete
with django_assert_num_queries(38): with django_assert_num_queries(39):
response = self.assert_retrieve_success(program) response = self.assert_retrieve_success(program)
assert response.data == self.serialize_program(program) assert response.data == self.serialize_program(program)
assert course_list == list(program.courses.all()) # pylint: disable=no-member assert course_list == list(program.courses.all()) # pylint: disable=no-member
...@@ -124,7 +124,7 @@ class TestProgramViewSet(SerializationMixin): ...@@ -124,7 +124,7 @@ class TestProgramViewSet(SerializationMixin):
""" Verify the endpoint returns data for a program even if the program's courses have no course runs. """ """ Verify the endpoint returns data for a program even if the program's courses have no course runs. """
course = CourseFactory(partner=self.partner) course = CourseFactory(partner=self.partner)
program = ProgramFactory(courses=[course], partner=self.partner) program = ProgramFactory(courses=[course], partner=self.partner)
with django_assert_num_queries(25): with django_assert_num_queries(26):
response = self.assert_retrieve_success(program) response = self.assert_retrieve_success(program)
assert response.data == self.serialize_program(program) assert response.data == self.serialize_program(program)
......
from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from course_discovery.apps.api import serializers from course_discovery.apps.api import serializers
from course_discovery.apps.api.exceptions import InvalidPartnerError
from course_discovery.apps.core.models import Partner
User = get_user_model() User = get_user_model()
......
...@@ -11,12 +11,12 @@ from rest_framework.permissions import IsAuthenticated ...@@ -11,12 +11,12 @@ from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from course_discovery.apps.api import filters, serializers from course_discovery.apps.api import filters, mixins, serializers
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 Course, CourseRun, Program from course_discovery.apps.course_metadata.models import Course, CourseRun, Program
class BaseHaystackViewSet(FacetMixin, HaystackViewSet): class BaseHaystackViewSet(mixins.DetailMixin, FacetMixin, HaystackViewSet):
document_uid_field = 'key' document_uid_field = 'key'
facet_filter_backends = [filters.HaystackFacetFilterWithQueries, filters.HaystackFilter, OrderingFilter] facet_filter_backends = [filters.HaystackFacetFilterWithQueries, filters.HaystackFilter, OrderingFilter]
ordering_fields = ('start',) ordering_fields = ('start',)
...@@ -93,27 +93,31 @@ class BaseHaystackViewSet(FacetMixin, HaystackViewSet): ...@@ -93,27 +93,31 @@ class BaseHaystackViewSet(FacetMixin, HaystackViewSet):
class CourseSearchViewSet(BaseHaystackViewSet): class CourseSearchViewSet(BaseHaystackViewSet):
facet_serializer_class = serializers.CourseFacetSerializer
index_models = (Course,) index_models = (Course,)
detail_serializer_class = serializers.CourseSearchModelSerializer
facet_serializer_class = serializers.CourseFacetSerializer
serializer_class = serializers.CourseSearchSerializer serializer_class = serializers.CourseSearchSerializer
class CourseRunSearchViewSet(BaseHaystackViewSet): class CourseRunSearchViewSet(BaseHaystackViewSet):
facet_serializer_class = serializers.CourseRunFacetSerializer
index_models = (CourseRun,) index_models = (CourseRun,)
detail_serializer_class = serializers.CourseRunSearchModelSerializer
facet_serializer_class = serializers.CourseRunFacetSerializer
serializer_class = serializers.CourseRunSearchSerializer serializer_class = serializers.CourseRunSearchSerializer
class ProgramSearchViewSet(BaseHaystackViewSet): class ProgramSearchViewSet(BaseHaystackViewSet):
document_uid_field = 'uuid' document_uid_field = 'uuid'
lookup_field = 'uuid' lookup_field = 'uuid'
facet_serializer_class = serializers.ProgramFacetSerializer
index_models = (Program,) index_models = (Program,)
detail_serializer_class = serializers.ProgramSearchModelSerializer
facet_serializer_class = serializers.ProgramFacetSerializer
serializer_class = serializers.ProgramSearchSerializer serializer_class = serializers.ProgramSearchSerializer
class AggregateSearchViewSet(BaseHaystackViewSet): class AggregateSearchViewSet(BaseHaystackViewSet):
""" Search all content types. """ """ Search all content types. """
detail_serializer_class = serializers.AggregateSearchModelSerializer
facet_serializer_class = serializers.AggregateFacetSearchSerializer facet_serializer_class = serializers.AggregateFacetSearchSerializer
serializer_class = serializers.AggregateSearchSerializer serializer_class = serializers.AggregateSearchSerializer
......
...@@ -4,7 +4,6 @@ from __future__ import unicode_literals ...@@ -4,7 +4,6 @@ from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
SWITCH = 'use_company_name_as_utm_source_value' SWITCH = 'use_company_name_as_utm_source_value'
......
...@@ -3,7 +3,6 @@ import logging ...@@ -3,7 +3,6 @@ import logging
import pytest import pytest
import responses import responses
from django.conf import settings from django.conf import settings
from haystack import connections as haystack_connections from haystack import connections as haystack_connections
......
...@@ -26,15 +26,13 @@ from taggit_autosuggest.managers import TaggableManager ...@@ -26,15 +26,13 @@ from taggit_autosuggest.managers import TaggableManager
from course_discovery.apps.core.models import Currency, Partner from course_discovery.apps.core.models import Currency, Partner
from course_discovery.apps.course_metadata.choices import CourseRunPacing, CourseRunStatus, ProgramStatus, ReportingType from course_discovery.apps.course_metadata.choices import CourseRunPacing, CourseRunStatus, ProgramStatus, ReportingType
from course_discovery.apps.course_metadata.publishers import ( from course_discovery.apps.course_metadata.publishers import (
CourseRunMarketingSitePublisher, CourseRunMarketingSitePublisher, ProgramMarketingSitePublisher
ProgramMarketingSitePublisher
) )
from course_discovery.apps.course_metadata.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet from course_discovery.apps.course_metadata.query import CourseQuerySet, CourseRunQuerySet, ProgramQuerySet
from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath, clean_query, custom_render_variations from course_discovery.apps.course_metadata.utils import UploadToFieldNamePath, clean_query, custom_render_variations
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.utils import VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY from course_discovery.apps.publisher.utils import VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -8,12 +8,7 @@ from django.utils.text import slugify ...@@ -8,12 +8,7 @@ from django.utils.text import slugify
from course_discovery.apps.course_metadata.choices import CourseRunStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus
from course_discovery.apps.course_metadata.exceptions import ( from course_discovery.apps.course_metadata.exceptions import (
AliasCreateError, AliasCreateError, AliasDeleteError, FormRetrievalError, NodeCreateError, NodeDeleteError, NodeEditError,
AliasDeleteError,
FormRetrievalError,
NodeCreateError,
NodeDeleteError,
NodeEditError,
NodeLookupError NodeLookupError
) )
from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClient from course_discovery.apps.course_metadata.utils import MarketingSiteAPIClient
......
...@@ -6,6 +6,26 @@ from opaque_keys.edx.keys import CourseKey ...@@ -6,6 +6,26 @@ from opaque_keys.edx.keys import CourseKey
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.models import Course, CourseRun, Program from course_discovery.apps.course_metadata.models import Course, CourseRun, Program
BASE_SEARCH_INDEX_FIELDS = (
'aggregation_key',
'content_type',
'text',
)
BASE_PROGRAM_FIELDS = (
'card_image_url',
'language',
'marketing_url',
'partner',
'published',
'status',
'subtitle',
'text',
'title',
'type',
'uuid'
)
# http://django-haystack.readthedocs.io/en/v2.5.0/boost.html#field-boost # http://django-haystack.readthedocs.io/en/v2.5.0/boost.html#field-boost
# Boost title over all other parameters (multiplicative) # Boost title over all other parameters (multiplicative)
# The max boost received from our boosting functions is ~6. # The max boost received from our boosting functions is ~6.
......
...@@ -270,6 +270,7 @@ class ProgramFactory(factory.django.DjangoModelFactory): ...@@ -270,6 +270,7 @@ class ProgramFactory(factory.django.DjangoModelFactory):
banner_image_url = FuzzyText(prefix='https://example.com/program/banner') banner_image_url = FuzzyText(prefix='https://example.com/program/banner')
card_image_url = FuzzyText(prefix='https://example.com/program/card') card_image_url = FuzzyText(prefix='https://example.com/program/card')
partner = factory.SubFactory(PartnerFactory) partner = factory.SubFactory(PartnerFactory)
video = factory.SubFactory(VideoFactory)
overview = FuzzyText() overview = FuzzyText()
total_hours_of_effort = FuzzyInteger(2) total_hours_of_effort = FuzzyInteger(2)
weeks_to_complete = FuzzyInteger(1) weeks_to_complete = FuzzyInteger(1)
......
...@@ -7,18 +7,11 @@ import responses ...@@ -7,18 +7,11 @@ import responses
from course_discovery.apps.core.tests.factories import PartnerFactory from course_discovery.apps.core.tests.factories import PartnerFactory
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.exceptions import ( from course_discovery.apps.course_metadata.exceptions import (
AliasCreateError, AliasCreateError, AliasDeleteError, FormRetrievalError, NodeCreateError, NodeDeleteError, NodeEditError,
AliasDeleteError,
FormRetrievalError,
NodeCreateError,
NodeDeleteError,
NodeEditError,
NodeLookupError NodeLookupError
) )
from course_discovery.apps.course_metadata.publishers import ( from course_discovery.apps.course_metadata.publishers import (
BaseMarketingSitePublisher, BaseMarketingSitePublisher, CourseRunMarketingSitePublisher, ProgramMarketingSitePublisher
CourseRunMarketingSitePublisher,
ProgramMarketingSitePublisher
) )
from course_discovery.apps.course_metadata.tests import toggle_switch from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory from course_discovery.apps.course_metadata.tests.factories import CourseRunFactory, ProgramFactory
......
...@@ -4,10 +4,10 @@ import urllib.parse ...@@ -4,10 +4,10 @@ import urllib.parse
import pytz import pytz
from django.urls import reverse from django.urls import reverse
from course_discovery.apps.api.v1.tests.test_views.mixins import APITestCase from course_discovery.apps.api.v1.tests.test_views.mixins import (
from course_discovery.apps.api.v1.tests.test_views.test_search import ( APITestCase, LoginMixin, SerializationMixin, SynonymTestMixin
ElasticsearchTestMixin, LoginMixin, SerializationMixin, SynonymTestMixin
) )
from course_discovery.apps.api.v1.tests.test_views.test_search import ElasticsearchTestMixin
from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus from course_discovery.apps.course_metadata.choices import CourseRunStatus, ProgramStatus
from course_discovery.apps.course_metadata.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory from course_discovery.apps.course_metadata.tests.factories import CourseFactory, CourseRunFactory, ProgramFactory
from course_discovery.apps.edx_catalog_extensions.api.serializers import DistinctCountsAggregateFacetSearchSerializer from course_discovery.apps.edx_catalog_extensions.api.serializers import DistinctCountsAggregateFacetSearchSerializer
...@@ -125,9 +125,9 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin, ...@@ -125,9 +125,9 @@ class DistinctCountsAggregateSearchViewSetTests(SerializationMixin, LoginMixin,
for record in objects['results']: for record in objects['results']:
if record['content_type'] == 'courserun': if record['content_type'] == 'courserun':
assert record == self.serialize_course_run(course_runs[str(record['key'])]) assert record == self.serialize_course_run_search(course_runs[str(record['key'])])
else: else:
assert record == self.serialize_program(programs[str(record['uuid'])]) assert record == self.serialize_program_search(programs[str(record['uuid'])])
def test_response_with_search_query(self): def test_response_with_search_query(self):
""" Verify that the response is accurate when a search query is passed.""" """ Verify that the response is accurate when a search query is passed."""
......
import elasticsearch import elasticsearch
from django.conf import settings from django.conf import settings
from haystack.backends.elasticsearch_backend import ElasticsearchSearchQuery from haystack.backends.elasticsearch_backend import ElasticsearchSearchQuery
from haystack.models import SearchResult from haystack.models import SearchResult
......
from django.conf import settings from django.conf import settings
from haystack.query import SearchQuerySet from haystack.query import SearchQuerySet
from course_discovery.apps.edx_haystack_extensions.distinct_counts.backends import DistinctCountsSearchQuery from course_discovery.apps.edx_haystack_extensions.distinct_counts.backends import DistinctCountsSearchQuery
......
...@@ -4,7 +4,6 @@ from django.conf import settings ...@@ -4,7 +4,6 @@ from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from haystack import connections as haystack_connections from haystack import connections as haystack_connections
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -6,16 +6,18 @@ from simple_history.admin import SimpleHistoryAdmin ...@@ -6,16 +6,18 @@ from simple_history.admin import SimpleHistoryAdmin
from course_discovery.apps.publisher.assign_permissions import assign_permissions from course_discovery.apps.publisher.assign_permissions import assign_permissions
from course_discovery.apps.publisher.choices import InternalUserRole from course_discovery.apps.publisher.choices import InternalUserRole
from course_discovery.apps.publisher.constants import (INTERNAL_USER_GROUP_NAME, PARTNER_MANAGER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PROJECT_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, INTERNAL_USER_GROUP_NAME, PARTNER_MANAGER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME,
REVIEWER_GROUP_NAME) REVIEWER_GROUP_NAME
)
from course_discovery.apps.publisher.forms import ( from course_discovery.apps.publisher.forms import (
CourseRunAdminForm, CourseRunStateAdminForm, CourseStateAdminForm, OrganizationExtensionForm, CourseRunAdminForm, CourseRunStateAdminForm, CourseStateAdminForm, OrganizationExtensionForm,
PublisherUserCreationForm, UserAttributesAdminForm PublisherUserCreationForm, UserAttributesAdminForm
) )
from course_discovery.apps.publisher.models import (Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, from course_discovery.apps.publisher.models import (
CourseUserRole, OrganizationExtension, OrganizationUserRole, Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension,
PublisherUser, Seat, UserAttributes) OrganizationUserRole, PublisherUser, Seat, UserAttributes
)
@admin.register(CourseUserRole) @admin.register(CourseUserRole)
......
...@@ -13,9 +13,10 @@ from rest_framework import serializers ...@@ -13,9 +13,10 @@ from rest_framework import serializers
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.emails import (send_change_role_assignment_email, from course_discovery.apps.publisher.emails import (
send_email_for_studio_instance_created, send_email_preview_accepted, send_change_role_assignment_email, send_email_for_studio_instance_created, send_email_preview_accepted,
send_email_preview_page_is_available) send_email_preview_page_is_available
)
from course_discovery.apps.publisher.models import CourseRun, CourseRunState, CourseState, CourseUserRole from course_discovery.apps.publisher.models import CourseRun, CourseRunState, CourseState, CourseUserRole
......
...@@ -11,14 +11,16 @@ from course_discovery.apps.core.tests.helpers import make_image_file ...@@ -11,14 +11,16 @@ from course_discovery.apps.core.tests.helpers import make_image_file
from course_discovery.apps.course_metadata.tests import toggle_switch from course_discovery.apps.course_metadata.tests import toggle_switch
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunSerializer, from course_discovery.apps.publisher.api.serializers import (
CourseRunStateSerializer, CourseStateSerializer, CourseRevisionSerializer, CourseRunSerializer, CourseRunStateSerializer, CourseStateSerializer,
CourseUserRoleSerializer, GroupUserSerializer) CourseUserRoleSerializer, GroupUserSerializer
)
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.models import CourseRun, CourseState, Seat from course_discovery.apps.publisher.models import CourseRun, CourseState, Seat
from course_discovery.apps.publisher.tests.factories import (CourseFactory, CourseRunFactory, CourseRunStateFactory, from course_discovery.apps.publisher.tests.factories import (
CourseStateFactory, CourseUserRoleFactory, CourseFactory, CourseRunFactory, CourseRunStateFactory, CourseStateFactory, CourseUserRoleFactory,
OrganizationExtensionFactory, SeatFactory) OrganizationExtensionFactory, SeatFactory
)
class CourseUserRoleSerializerTests(SiteMixin, TestCase): class CourseUserRoleSerializerTests(SiteMixin, TestCase):
......
import pytest import pytest
from course_discovery.apps.core.utils import serialize_datetime from course_discovery.apps.core.utils import serialize_datetime
from course_discovery.apps.publisher.api.utils import (serialize_entitlement_for_ecommerce_api, from course_discovery.apps.publisher.api.utils import (
serialize_seat_for_ecommerce_api) serialize_entitlement_for_ecommerce_api, serialize_seat_for_ecommerce_api
)
from course_discovery.apps.publisher.models import Seat from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, SeatFactory from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, SeatFactory
......
...@@ -22,8 +22,9 @@ from course_discovery.apps.ietf_language_tags.models import LanguageTag ...@@ -22,8 +22,9 @@ from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api import views from course_discovery.apps.publisher.api import views
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME from course_discovery.apps.publisher.constants import ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, from course_discovery.apps.publisher.models import (
OrganizationExtension, Seat) Course, CourseRun, CourseRunState, CourseState, OrganizationExtension, Seat
)
from course_discovery.apps.publisher.tests import JSON_CONTENT_TYPE, factories from course_discovery.apps.publisher.tests import JSON_CONTENT_TYPE, factories
......
""" Publisher API URLs. """ """ Publisher API URLs. """
from django.conf.urls import include, url from django.conf.urls import include, url
from course_discovery.apps.publisher.api.views import (AcceptAllRevisionView, ChangeCourseRunStateView, from course_discovery.apps.publisher.api.views import (
ChangeCourseStateView, CourseRevisionDetailView, AcceptAllRevisionView, ChangeCourseRunStateView, ChangeCourseStateView, CourseRevisionDetailView,
CourseRoleAssignmentView, CoursesAutoComplete, CourseRoleAssignmentView, CoursesAutoComplete, OrganizationGroupUserView, RevertCourseRevisionView,
OrganizationGroupUserView, RevertCourseRevisionView, UpdateCourseRunView
UpdateCourseRunView) )
urlpatterns = [ urlpatterns = [
url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'), url(r'^course_role_assignments/(?P<pk>\d+)/$', CourseRoleAssignmentView.as_view(), name='course_role_assignments'),
......
...@@ -18,8 +18,9 @@ from course_discovery.apps.course_metadata.models import Seat as DiscoverySeat ...@@ -18,8 +18,9 @@ from course_discovery.apps.course_metadata.models import Seat as DiscoverySeat
from course_discovery.apps.course_metadata.models import CourseRun, SeatType, Video from course_discovery.apps.course_metadata.models import CourseRun, SeatType, Video
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory, PersonFactory
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.api.utils import (serialize_entitlement_for_ecommerce_api, from course_discovery.apps.publisher.api.utils import (
serialize_seat_for_ecommerce_api) serialize_entitlement_for_ecommerce_api, serialize_seat_for_ecommerce_api
)
from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet from course_discovery.apps.publisher.api.v1.views import CourseRunViewSet
from course_discovery.apps.publisher.models import CourseEntitlement, Seat from course_discovery.apps.publisher.models import CourseEntitlement, Seat
from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, CourseRunFactory, SeatFactory from course_discovery.apps.publisher.tests.factories import CourseEntitlementFactory, CourseRunFactory, SeatFactory
......
...@@ -11,14 +11,17 @@ from rest_framework.views import APIView ...@@ -11,14 +11,17 @@ from rest_framework.views import APIView
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.api.paginations import LargeResultsSetPagination from course_discovery.apps.publisher.api.paginations import LargeResultsSetPagination
from course_discovery.apps.publisher.api.permissions import (CanViewAssociatedCourse, InternalUserPermission, from course_discovery.apps.publisher.api.permissions import (
PublisherUserPermission) CanViewAssociatedCourse, InternalUserPermission, PublisherUserPermission
from course_discovery.apps.publisher.api.serializers import (CourseRevisionSerializer, CourseRunSerializer, )
CourseRunStateSerializer, CourseStateSerializer, from course_discovery.apps.publisher.api.serializers import (
CourseUserRoleSerializer, GroupUserSerializer) CourseRevisionSerializer, CourseRunSerializer, CourseRunStateSerializer, CourseStateSerializer,
CourseUserRoleSerializer, GroupUserSerializer
)
from course_discovery.apps.publisher.forms import CourseForm from course_discovery.apps.publisher.forms import CourseForm
from course_discovery.apps.publisher.models import (Course, CourseRun, CourseRunState, CourseState, CourseUserRole, from course_discovery.apps.publisher.models import (
OrganizationExtension, PublisherUser) Course, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension, PublisherUser
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from guardian.shortcuts import assign_perm from guardian.shortcuts import assign_perm
from course_discovery.apps.publisher.constants import (GENERAL_STAFF_GROUP_NAME, LEGAL_TEAM_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PARTNER_SUPPORT_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, GENERAL_STAFF_GROUP_NAME, LEGAL_TEAM_GROUP_NAME, PARTNER_SUPPORT_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME,
REVIEWER_GROUP_NAME) REVIEWER_GROUP_NAME
)
from course_discovery.apps.publisher.models import OrganizationExtension from course_discovery.apps.publisher.models import OrganizationExtension
......
...@@ -3,8 +3,9 @@ from __future__ import unicode_literals ...@@ -3,8 +3,9 @@ from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
from course_discovery.apps.publisher.constants import (PARTNER_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
REVIEWER_GROUP_NAME) PARTNER_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME
)
GROUPS = [PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME] GROUPS = [PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME]
......
...@@ -4,8 +4,10 @@ from __future__ import unicode_literals ...@@ -4,8 +4,10 @@ from __future__ import unicode_literals
from django.db import migrations from django.db import migrations
from course_discovery.apps.publisher.constants import (INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME) INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME
)
GROUPS = [INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME] GROUPS = [INTERNAL_USER_GROUP_NAME, PARTNER_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME, PUBLISHER_GROUP_NAME]
......
...@@ -29,7 +29,6 @@ from course_discovery.apps.publisher.choices import ( ...@@ -29,7 +29,6 @@ from course_discovery.apps.publisher.choices import (
CourseRunStateChoices, CourseStateChoices, InternalUserRole, PublisherUserRole CourseRunStateChoices, CourseStateChoices, InternalUserRole, PublisherUserRole
) )
from course_discovery.apps.publisher.utils import is_email_notification_enabled, is_internal_user, is_publisher_admin from course_discovery.apps.publisher.utils import is_email_notification_enabled, is_internal_user, is_publisher_admin
from course_discovery.apps.publisher.validators import ImageMultiSizeValidator from course_discovery.apps.publisher.validators import ImageMultiSizeValidator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
......
...@@ -12,9 +12,10 @@ from course_discovery.apps.course_metadata.choices import CourseRunPacing ...@@ -12,9 +12,10 @@ from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.tests import factories from course_discovery.apps.course_metadata.tests import factories
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.models import (Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, from course_discovery.apps.publisher.models import (
CourseUserRole, OrganizationExtension, OrganizationUserRole, Seat, Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension,
UserAttributes) OrganizationUserRole, Seat, UserAttributes
)
class CourseFactory(factory.DjangoModelFactory): class CourseFactory(factory.DjangoModelFactory):
......
...@@ -8,8 +8,9 @@ from course_discovery.apps.api.tests.mixins import SiteMixin ...@@ -8,8 +8,9 @@ from course_discovery.apps.api.tests.mixins import SiteMixin
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory from course_discovery.apps.course_metadata.tests.factories import OrganizationFactory
from course_discovery.apps.publisher.choices import PublisherUserRole from course_discovery.apps.publisher.choices import PublisherUserRole
from course_discovery.apps.publisher.constants import (PARTNER_MANAGER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME) PARTNER_MANAGER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, PUBLISHER_GROUP_NAME, REVIEWER_GROUP_NAME
)
from course_discovery.apps.publisher.forms import CourseRunAdminForm from course_discovery.apps.publisher.forms import CourseRunAdminForm
from course_discovery.apps.publisher.models import CourseRun, OrganizationExtension from course_discovery.apps.publisher.models import CourseRun, OrganizationExtension
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
......
...@@ -16,8 +16,9 @@ from course_discovery.apps.course_metadata.tests.factories import OrganizationFa ...@@ -16,8 +16,9 @@ from course_discovery.apps.course_metadata.tests.factories import OrganizationFa
from course_discovery.apps.ietf_language_tags.models import LanguageTag from course_discovery.apps.ietf_language_tags.models import LanguageTag
from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, CourseStateChoices, PublisherUserRole
from course_discovery.apps.publisher.mixins import check_course_organization_permission from course_discovery.apps.publisher.mixins import check_course_organization_permission
from course_discovery.apps.publisher.models import (Course, CourseUserRole, OrganizationExtension, from course_discovery.apps.publisher.models import (
OrganizationUserRole, Seat) Course, CourseUserRole, OrganizationExtension, OrganizationUserRole, Seat
)
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
......
...@@ -9,16 +9,18 @@ from guardian.shortcuts import assign_perm ...@@ -9,16 +9,18 @@ from guardian.shortcuts import assign_perm
from mock import Mock from mock import Mock
from course_discovery.apps.core.tests.factories import UserFactory from course_discovery.apps.core.tests.factories import UserFactory
from course_discovery.apps.publisher.constants import (ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PROJECT_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME) ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME, REVIEWER_GROUP_NAME
from course_discovery.apps.publisher.mixins import (check_course_organization_permission, check_roles_access, )
publisher_user_required) from course_discovery.apps.publisher.mixins import (
check_course_organization_permission, check_roles_access, publisher_user_required
)
from course_discovery.apps.publisher.models import OrganizationExtension from course_discovery.apps.publisher.models import OrganizationExtension
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
from course_discovery.apps.publisher.utils import (get_internal_users, has_role_for_course, from course_discovery.apps.publisher.utils import (
is_email_notification_enabled, is_internal_user, get_internal_users, has_role_for_course, is_email_notification_enabled, is_internal_user,
is_project_coordinator_user, is_publisher_admin, is_publisher_user, is_project_coordinator_user, is_publisher_admin, is_publisher_user, make_bread_crumbs, parse_datetime_field
make_bread_crumbs, parse_datetime_field) )
@ddt.ddt @ddt.ddt
......
...@@ -6,8 +6,9 @@ import ddt ...@@ -6,8 +6,9 @@ import ddt
from django.test import TestCase from django.test import TestCase
from course_discovery.apps.course_metadata.choices import CourseRunPacing from course_discovery.apps.course_metadata.choices import CourseRunPacing
from course_discovery.apps.course_metadata.tests.factories import (OrganizationFactory, PersonFactory, from course_discovery.apps.course_metadata.tests.factories import (
PersonSocialNetworkFactory, PositionFactory) OrganizationFactory, PersonFactory, PersonSocialNetworkFactory, PositionFactory
)
from course_discovery.apps.publisher.choices import CourseRunStateChoices, PublisherUserRole from course_discovery.apps.publisher.choices import CourseRunStateChoices, PublisherUserRole
from course_discovery.apps.publisher.models import Seat from course_discovery.apps.publisher.models import Seat
from course_discovery.apps.publisher.tests import factories from course_discovery.apps.publisher.tests import factories
......
""" Publisher Utils.""" """ Publisher Utils."""
import re import re
from dateutil import parser from dateutil import parser
from course_discovery.apps.core.models import User from course_discovery.apps.core.models import User
from course_discovery.apps.publisher.constants import (ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, from course_discovery.apps.publisher.constants import (
PROJECT_COORDINATOR_GROUP_NAME) ADMIN_GROUP_NAME, INTERNAL_USER_GROUP_NAME, PROJECT_COORDINATOR_GROUP_NAME
)
VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY = re.compile(r'^[a-zA-Z0-9._-]*$') VALID_CHARS_IN_COURSE_NUM_AND_ORG_KEY = re.compile(r'^[a-zA-Z0-9._-]*$')
......
...@@ -32,8 +32,8 @@ from course_discovery.apps.publisher.forms import ( ...@@ -32,8 +32,8 @@ from course_discovery.apps.publisher.forms import (
AdminImportCourseForm, CourseEntitlementForm, CourseForm, CourseRunForm, CourseSearchForm, SeatForm AdminImportCourseForm, CourseEntitlementForm, CourseForm, CourseRunForm, CourseSearchForm, SeatForm
) )
from course_discovery.apps.publisher.models import ( from course_discovery.apps.publisher.models import (
Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, Course, CourseEntitlement, CourseRun, CourseRunState, CourseState, CourseUserRole, OrganizationExtension,
OrganizationExtension, PublisherUser, Seat, UserAttributes PublisherUser, Seat, UserAttributes
) )
from course_discovery.apps.publisher.utils import ( from course_discovery.apps.publisher.utils import (
get_internal_users, has_role_for_course, is_internal_user, is_project_coordinator_user, is_publisher_admin, get_internal_users, has_role_for_course, is_internal_user, is_project_coordinator_user, is_publisher_admin,
......
import logging import logging
import waffle import waffle
from django.db import models, transaction from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
......
...@@ -3,7 +3,6 @@ URLs for the course publisher comments views. ...@@ -3,7 +3,6 @@ URLs for the course publisher comments views.
""" """
from django.conf.urls import include, url from django.conf.urls import include, url
urlpatterns = [ urlpatterns = [
url(r'^api/', include('course_discovery.apps.publisher_comments.api.urls', namespace='api')), url(r'^api/', include('course_discovery.apps.publisher_comments.api.urls', namespace='api')),
] ]
from course_discovery.settings.devstack import * from course_discovery.settings.devstack import *
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
from course_discovery.settings.shared.test import * # isort:skip from course_discovery.settings.shared.test import * # isort:skip
......
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