Skip to content
Merged
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
8 changes: 5 additions & 3 deletions cms/djangoapps/contentstore/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
from cms.djangoapps.contentstore.courseware_index import (
CourseAboutSearchIndexer,
CoursewareSearchIndexer,
LibrarySearchIndexer
LibrarySearchIndexer,
)
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from common.djangoapps.util.module_utils import yield_dynamic_descriptor_descendants
from lms.djangoapps.grades.api import task_compute_all_grades_for_course
from openedx.core.djangoapps.content.learning_sequences.api import key_supports_outlines
from openedx.core.djangoapps.discussions.tasks import update_discussions_settings_from_course_task
from openedx.core.lib.gating import api as gating_api
from xmodule.modulestore.django import SignalHandler, modulestore

from .signals import GRADING_POLICY_CHANGED

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -64,11 +64,13 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
# Push the course outline to learning_sequences asynchronously.
update_outline_from_modulestore_task.delay(course_key_str)

# Finally call into the course search subsystem
# Finally, call into the course search subsystem
# to kick off an indexing action
if CoursewareSearchIndexer.indexing_is_enabled() and CourseAboutSearchIndexer.indexing_is_enabled():
update_search_index.delay(course_key_str, datetime.now(UTC).isoformat())

update_discussions_settings_from_course_task.delay(course_key_str)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be triggered every time? Or should it be triggered only if the course has in-context discussions enabled?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want to add that logic to a generic place. So that logic is in the task itself. Additionally, there may be other things that this code can do which apply to providers that don't support in-context discussions as well.



@receiver(SignalHandler.course_deleted)
def listen_for_course_delete(sender, course_key, **kwargs): # pylint: disable=unused-argument
Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/course_wiki/tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@


from django.conf import settings
from django.utils.translation import gettext_noop
from django.utils.translation import gettext_noop as _

from lms.djangoapps.courseware.tabs import EnrolledTab

Expand All @@ -16,7 +16,7 @@ class WikiTab(EnrolledTab):
"""

type = "wiki"
title = gettext_noop('Wiki')
title = _('Wiki')
view_name = "course_wiki"
is_hideable = True
is_default = False
Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/edxnotes/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_noop as _
from django.utils.translation import gettext_noop as _
from opaque_keys.edx.keys import CourseKey
from xmodule.modulestore.django import modulestore

Expand Down
2 changes: 1 addition & 1 deletion lms/djangoapps/teams/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.translation import ugettext_noop as _
from django.utils.translation import gettext_noop as _
from opaque_keys.edx.keys import CourseKey

from lms.djangoapps.courseware.tabs import EnrolledTab
Expand Down
3 changes: 3 additions & 0 deletions openedx/core/djangoapps/discussions/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ class DiscussionsConfig(AppConfig):
PluginSettings.CONFIG: {
},
}

def ready(self):
from . import handlers # pylint: disable=unused-import
45 changes: 45 additions & 0 deletions openedx/core/djangoapps/discussions/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""
Data classes for discussions
"""
from typing import List

import attr
from opaque_keys.edx.keys import CourseKey, UsageKey

# TODO: These data models will be moved to openedx_events. They are currently here to simplify the PR.


@attr.s(frozen=True)
class DiscussionTopicContext:
"""
Context for linking the external id for a discussion topic to its associated usage key.

The ``title`` is cached to improve the performance, since otherwise we'd
need to look it up in the course structure each time.

The ``group_id`` can be used for providers that don't internally support
cohorting but we can emulate that wuth different contexts for different groups.
"""
title = attr.ib(type=str)
usage_key = attr.ib(type=UsageKey, default=None)
group_id = attr.ib(type=int, default=None)
external_id = attr.ib(type=str, default=None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this external_id used for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The external_id is what will store the commentable id that is used by cs_comments_service. In the future, if we are dealing with another provider that supports in-context discussions, this can be used for them as well.



@attr.s(frozen=True)
class CourseDiscussionConfigurationData:
"""
Course configuration information for discussions.

Contains all the metadata needed to configure discussions for a course.

The ``contexts`` field contains all the contexts for which discussion
is to be enabled.
"""
course_key = attr.ib(type=CourseKey)
provider_type = attr.ib(type=str)
enable_in_context = attr.ib(type=bool, default=True)
enable_graded_units = attr.ib(type=bool, default=False)
unit_level_visibility = attr.ib(type=bool, default=False)
plugin_configuration = attr.ib(type=dict, default={})
contexts = attr.ib(type=List[DiscussionTopicContext], factory=list)
97 changes: 97 additions & 0 deletions openedx/core/djangoapps/discussions/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
"""
Signal handlers for discussions events
"""
import logging
from uuid import uuid4

from django.db import transaction

from openedx.core.djangoapps.discussions.data import CourseDiscussionConfigurationData
from openedx.core.djangoapps.discussions.models import (
DEFAULT_PROVIDER_TYPE,
DiscussionTopicLink,
DiscussionsConfiguration,
)
from openedx.core.djangoapps.discussions.signals import COURSE_DISCUSSIONS_UPDATED

log = logging.getLogger(__name__)


# pylint: disable=unused-argument
def handle_course_discussion_config_update(sender, configuration: CourseDiscussionConfigurationData, **kwargs):
"""
Updates the database models for course topics and configuration when settings are updated in the course.

Args:
sender: Ignored
configuration (CourseDiscussionConfigurationData): configuration data for the course

"""
update_course_discussion_config(configuration)


def update_course_discussion_config(configuration: CourseDiscussionConfigurationData):
"""
Update the database version of the configuration if it changes in the course structure.

This function accepts a discussion configuration object that represents the current
configuration and applies that state to the database. It will go over the list of topic
links in the configuration, find the corresponding topic link in the database and apply
any changes if needed. If a new topic link has been introduced it will create an entry.
If a topic has been removed, it will deactivate the entry.

When this runs on a new course it will create a new DiscussionConfiguration entry for
the course.

Args:
configuration (CourseDiscussionConfigurationData): configuration data for the course
"""
course_key = configuration.course_key
provider_id = configuration.provider_type or DEFAULT_PROVIDER_TYPE
new_topic_map = {
(topic_context.usage_key or topic_context.external_id): topic_context
for topic_context in configuration.contexts
}
with transaction.atomic():
log.info(f"Updating existing discussion topic links for {course_key}")
for topic_link in DiscussionTopicLink.objects.filter(
context_key=course_key, provider_id=provider_id,
):
lookup_key = topic_link.usage_key or topic_link.external_id
topic_context = new_topic_map.pop(lookup_key, None)
# TODO: handle deleting topics that are no longer in use
# currently this will simply not work for course-wide topics since deleting the link will
# remove access to all posts in the topic.
if topic_context is None:
topic_link.enabled_in_context = False
else:
topic_link.enabled_in_context = True
topic_link.title = topic_context.title
topic_link.save()
log.info(f"Creating new discussion topic links for {course_key}")

DiscussionTopicLink.objects.bulk_create([
DiscussionTopicLink(
context_key=course_key,
usage_key=topic_context.usage_key,
title=topic_context.title,
provider_id=provider_id,
external_id=topic_context.external_id or uuid4(),
enabled_in_context=True,
)
for topic_context in new_topic_map.values()
])

if not DiscussionsConfiguration.objects.filter(context_key=course_key).exists():
log.info(f"Course {course_key} doesn't have discussion configuration model yet. Creating a new one.")
DiscussionsConfiguration(
context_key=course_key,
provider_type=provider_id,
plugin_configuration=configuration.plugin_configuration,
enable_in_context=configuration.enable_in_context,
enable_graded_units=configuration.enable_graded_units,
unit_level_visibility=configuration.unit_level_visibility,
).save()


COURSE_DISCUSSIONS_UPDATED.connect(handle_course_discussion_config_update)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Generated by Django 3.2.8 on 2021-10-21 12:02

import django.db.models.deletion
import django_mysql.models
import opaque_keys.edx.django.models
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
('course_groups', '0003_auto_20170609_1455'),
('discussions', '0006_auto_20211006_0441'),
]

operations = [
migrations.AlterField(
model_name='providerfilter',
name='allow',
field=django_mysql.models.ListCharField(models.CharField(
choices=[('legacy', 'legacy'), ('openedx', 'openedx'), ('ed-discuss', 'ed-discuss'),
('inscribe', 'inscribe'), ('piazza', 'piazza'), ('yellowdig', 'yellowdig')], max_length=20),
blank=True,
help_text='Comma-separated list of providers to allow, eg: legacy,openedx,ed-discuss,inscribe,piazza,yellowdig',
max_length=63, size=3, verbose_name='Allow List'),
),
migrations.AlterField(
model_name='providerfilter',
name='deny',
field=django_mysql.models.ListCharField(models.CharField(
choices=[('legacy', 'legacy'), ('openedx', 'openedx'), ('ed-discuss', 'ed-discuss'),
('inscribe', 'inscribe'), ('piazza', 'piazza'), ('yellowdig', 'yellowdig')], max_length=20),
blank=True,
help_text='Comma-separated list of providers to deny, eg: legacy,openedx,ed-discuss,inscribe,piazza,yellowdig',
max_length=63, size=3, verbose_name='Deny List'),
),
migrations.CreateModel(
name='DiscussionTopicLink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('context_key', opaque_keys.edx.django.models.LearningContextKeyField(db_index=True,
help_text='Context key for context in which this discussion topic exists.', max_length=255,
verbose_name='Learning Context Key')),
('usage_key', opaque_keys.edx.django.models.UsageKeyField(blank=True, db_index=True,
help_text='Usage key for in-context discussion topic. Set to null for course-level topics.',
max_length=255, null=True)),
('title', models.CharField(help_text='Title for discussion topic.', max_length=255)),
('provider_id', models.CharField(help_text='Provider id for discussion provider.', max_length=32)),
('external_id', models.CharField(db_index=True,
help_text='Discussion context ID in external forum provider. e.g. commentable_id for cs_comments_service.',
max_length=255)),
('enabled_in_context', models.BooleanField(default=True,
help_text='Whether this topic should be shown in-context in the course.')),
('group', models.ForeignKey(blank=True, help_text='Group for divided discussions.', null=True,
on_delete=django.db.models.deletion.SET_NULL, to='course_groups.courseusergroup')),
],
),
]
Loading