Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1377,6 +1377,7 @@

# Discussion
'openedx.core.djangoapps.django_comment_common',
'openedx.core.djangoapps.discussions',

# for course creator table
'django.contrib.admin',
Expand Down
4 changes: 2 additions & 2 deletions common/lib/xmodule/xmodule/tabs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from abc import ABCMeta

import six
from django.core.files.storage import get_storage_class
from django.utils.module_loading import import_string
from six import text_type
from xblock.fields import List

Expand Down Expand Up @@ -283,7 +283,7 @@ def fragment_view(self):
Returns the view that will be used to render the fragment.
"""
if not self._fragment_view:
self._fragment_view = get_storage_class(self.fragment_view_name)()
self._fragment_view = import_string(self.fragment_view_name)()
return self._fragment_view

def render_to_fragment(self, request, course, **kwargs):
Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/ccx/tests/test_field_override_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
__test__ = True

# TODO: decrease query count as part of REVO-28
QUERY_COUNT = 31
QUERY_COUNT = 32
TEST_DATA = {
# (providers, course_width, enable_ccx, view_as_ccx): (
# # of sql queries to default,
Expand Down Expand Up @@ -273,7 +273,7 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True

# TODO: decrease query count as part of REVO-28
QUERY_COUNT = 31
QUERY_COUNT = 32

TEST_DATA = {
('no_overrides', 1, True, False): (QUERY_COUNT, 3),
Expand Down
12 changes: 6 additions & 6 deletions lms/djangoapps/courseware/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20

@ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 171),
(ModuleStoreEnum.Type.split, 4, 167),
(ModuleStoreEnum.Type.mongo, 10, 172),
(ModuleStoreEnum.Type.split, 4, 168),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
Expand Down Expand Up @@ -1425,8 +1425,8 @@ def test_view_certificate_link_hidden(self):
self.assertContains(resp, u"Download Your Certificate")

@ddt.data(
(True, 53),
(False, 52),
(True, 54),
(False, 53),
)
@ddt.unpack
def test_progress_queries_paced_courses(self, self_paced, query_count):
Expand All @@ -1439,8 +1439,8 @@ def test_progress_queries_paced_courses(self, self_paced, query_count):

@patch.dict(settings.FEATURES, {'ASSUME_ZERO_GRADE_IF_ABSENT_FOR_ALL_TESTS': False})
@ddt.data(
(False, 61, 42),
(True, 52, 37)
(False, 62, 43),
(True, 53, 38)
)
@ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent):
Expand Down
36 changes: 14 additions & 22 deletions lms/djangoapps/discussion/plugins.py
Original file line number Diff line number Diff line change
@@ -1,33 +1,25 @@
"""
Views handling read (GET) requests for the Discussion tab and inline discussions.
"""
from django.utils.translation import ugettext_lazy as _

from openedx.core.djangoapps.discussions.discussions_apps import DiscussionApp
from .django_comment_client.utils import is_discussion_enabled

from django.conf import settings
from django.utils.translation import ugettext_noop

import lms.djangoapps.discussion.django_comment_client.utils as utils
from lms.djangoapps.courseware.tabs import EnrolledTab
from xmodule.tabs import TabFragmentViewMixin


class DiscussionTab(TabFragmentViewMixin, EnrolledTab):
class CommentServiceDiscussionApp(DiscussionApp):
"""
A tab for the cs_comments_service forums.
Discussion Plugin app for cs_comments_service.
"""
name = "cs_comments"
friendly_name = _("edX Discussions")

capabilities = [

type = 'discussion'
title = ugettext_noop('Discussion')
priority = None
view_name = 'forum_form_discussion'
fragment_view_name = 'lms.djangoapps.discussion.views.DiscussionBoardFragmentView'
is_hideable = settings.FEATURES.get('ALLOW_HIDING_DISCUSSION_TAB', False)
is_default = False
body_class = 'discussion'
online_help_token = 'discussions'
]
course_tab_view = "lms.djangoapps.discussion.views.DiscussionBoardFragmentView"
course_tab_view_name = "forum_form_discussion"

@classmethod
def is_enabled(cls, course, user=None):
if not super(DiscussionTab, cls).is_enabled(course, user):
return False
return utils.is_discussion_enabled(course.id)
def is_enabled(cls, request=None, context_key=None, user=None):
return is_discussion_enabled(context_key)
1 change: 1 addition & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -2559,6 +2559,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring

# Discussion forums
'openedx.core.djangoapps.django_comment_common',
'openedx.core.djangoapps.discussions',

# Notes
'lms.djangoapps.edxnotes',
Expand Down
45 changes: 29 additions & 16 deletions openedx/core/djangoapps/config_model_utils/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from collections import defaultdict
from enum import Enum
from typing import Union

import crum
from config_models.models import ConfigurationModel, cache
Expand Down Expand Up @@ -45,6 +46,27 @@ def validate_course_in_org(value):
)


@request_cached()
def site_from_org(org: str) -> Union[Site, RequestSite]:
"""
Return the site associated with an org.

Args:
org (str): The org for which the associated site is to be fetched.

Returns:
Site object
"""
configuration = SiteConfiguration.get_configuration_for_org(org, select_related=['site'])
if configuration is None:
try:
return Site.objects.get(id=settings.SITE_ID)
except Site.DoesNotExist:
return RequestSite(crum.get_current_request())
else:
return configuration.site


class StackedConfigurationModel(ConfigurationModel):
"""
A ConfigurationModel that stacks Global, Site, Org, Course, and Course Run level
Expand Down Expand Up @@ -173,7 +195,7 @@ def current(cls, site=None, org=None, org_course=None, course_key=None): # pyli
org = cls._org_from_org_course(org_course)

if site is None and org is not None:
site = cls._site_from_org(org)
site = site_from_org(org)

stackable_fields = [cls._meta.get_field(field_name) for field_name in cls.STACKABLE_FIELDS]
field_defaults = {
Expand Down Expand Up @@ -234,7 +256,11 @@ def sort_key(override):
provenances[field.name] = Provenance.global_

current = cls(**values)
current.provenances = {field.name: provenances[field.name] for field in stackable_fields} # pylint: disable=attribute-defined-outside-init
# pylint: disable=attribute-defined-outside-init
current.provenances = {
field.name: provenances[field.name]
for field in stackable_fields
}
cache.set(cache_key_name, current, cls.cache_timeout)
return current

Expand Down Expand Up @@ -324,21 +350,8 @@ def _org_from_org_course(cls, org_course):
def _org_course_from_course_key(cls, course_key):
return u"{}+{}".format(course_key.org, course_key.course)

@classmethod
@request_cached()
def _site_from_org(cls, org):

configuration = SiteConfiguration.get_configuration_for_org(org, select_related=['site'])
if configuration is None:
try:
return Site.objects.get(id=settings.SITE_ID)
except Site.DoesNotExist:
return RequestSite(crum.get_current_request())
else:
return configuration.site

def clean(self):
# fail validation if more than one of site/org/course are specified simultaneously
""" Ensure that only one of site, org, org_course, and course are specified simultaneously. """
if len([arg for arg in [self.site, self.org, self.org_course, self.course] if arg is not None]) > 1:
raise ValidationError(
_('Configuration may not be specified at more than one level at once.')
Expand Down
1 change: 1 addition & 0 deletions openedx/core/djangoapps/discussions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
default_app_config = 'openedx.core.djangoapps.discussions.apps.DiscussionsConfig'
12 changes: 12 additions & 0 deletions openedx/core/djangoapps/discussions/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django.contrib import admin

from .models import DiscussionProviderConfig, LearningContextDiscussionConfig


class DiscussionProviderConfigAdminModel(admin.ModelAdmin):
search_fields = ("name", "provider", "config")
list_filter = ("restrict_to_site", "restrict_to_org", "provider")


admin.site.register(DiscussionProviderConfig, DiscussionProviderConfigAdminModel)
admin.site.register(LearningContextDiscussionConfig)
Empty file.
119 changes: 119 additions & 0 deletions openedx/core/djangoapps/discussions/api/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
from typing import Iterable, List, Optional

from django.contrib.sites.models import Site
from django.db.models import Q
from opaque_keys.edx.keys import CourseKey

from .data import CourseDiscussionConfigData, DiscussionPluginConfigData
from ..models import DiscussionProviderConfig, LearningContextDiscussionConfig
from ...config_model_utils.models import site_from_org


def _org_and_site_from_course_key(context_key: CourseKey):
org_key = getattr(context_key, 'org', None)
site = site_from_org(org_key)
return org_key, site


def _get_discussion_plugin_config_objects(org_key: str, site: Site) -> Iterable[DiscussionProviderConfig]:
return DiscussionProviderConfig.objects.filter(
Q(restrict_to_org__isnull=True, restrict_to_site__isnull=True) |
Q(restrict_to_org__short_name=org_key) |
Q(restrict_to_site=site)
)


def get_course_discussion_config_options(course_key: CourseKey) -> List[DiscussionPluginConfigData]:
"""
Returns the available discussion configuration options for provided course.

Args:
course_key (CourseKey): Learning context, currently only a course

Returns:
A list of :class:`DiscussionConfigData` objects.

"""
org_key, site = _org_and_site_from_course_key(course_key)
return [
DiscussionPluginConfigData(
name=discussion_config.name,
provider=discussion_config.provider,
config=discussion_config.config,
)
for discussion_config in _get_discussion_plugin_config_objects(org_key, site)
]


def get_course_discussion_config(course_key: CourseKey) -> Optional[CourseDiscussionConfigData]:
"""
Returns the active discussion configuration for the course.

Args:
course_key (CourseKey): Learning context, currently only a course

Returns:
A :class:`CourseDiscussionConfigData` object with the active configuration for this course.
Returns `None` if a discussion tool isn't configured for the course yet.

"""
try:
course_config = LearningContextDiscussionConfig.objects.get(pk=course_key)
except LearningContextDiscussionConfig.DoesNotExist:
return None

provider_config = course_config.provider_config
if not provider_config:
return CourseDiscussionConfigData(
course_key=course_key,
config_name=None,
provider=None,
config=None,
enabled=False,
)
merged_config = provider_config.config
merged_config.update(course_config.config_overrides)
return CourseDiscussionConfigData(
course_key=course_key,
config_name=provider_config.name,
provider=provider_config.provider,
config=merged_config,
enabled=course_config.enabled,
)


def update_course_discussion_config(
course_key: CourseKey,
updated_config: dict
) -> Optional[CourseDiscussionConfigData]:
"""
Updates the configuration for the specified course.

Args:
course_key (CourseKey): Learning context, currently only a course
updated_config (dict): Update configuration to save for specified course

Returns:
A :class:`CourseDiscussionConfigData` object with the active configuration for this course.
Returns `None` if a discussion tool isn't configured for the course yet.

"""
try:
course_config = LearningContextDiscussionConfig.objects.get(pk=course_key)
except LearningContextDiscussionConfig.DoesNotExist:
raise CourseDiscussionConfigData.DoesNotExist

provider_config = course_config.provider_config

if not provider_config:
raise CourseDiscussionConfigData.DoesNotExist

course_config.config_overrides = updated_config
course_config.save()
return CourseDiscussionConfigData(
course_key=course_key,
config_name=provider_config.name,
provider=provider_config.provider,
config=updated_config,
enabled=course_config.enabled,
)
36 changes: 36 additions & 0 deletions openedx/core/djangoapps/discussions/api/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import attr
from opaque_keys.edx.keys import CourseKey


class ObjectDoesNotExist(Exception):
"""
Imitating Django model conventions, we put a subclass of this in some of our
data classes to indicate when something is not found.
"""


@attr.s(frozen=True)
class DiscussionPluginConfigData:
"""
Discussion Plugin Configuration Data Object
"""

name = attr.ib(type=str)
provider = attr.ib(type=str)
config = attr.ib(type=dict)


@attr.s(frozen=True)
class CourseDiscussionConfigData:
"""
Course Discussion Configuration Data Object
"""

course_key = attr.ib(type=CourseKey)
config_name = attr.ib(type=str)
provider = attr.ib(type=str)
config = attr.ib(type=dict)
enabled = attr.ib(type=bool)

class DoesNotExist(ObjectDoesNotExist):
pass
Loading