From 5bb56fe45f279016216609f8dc0e8c0a7e09021e Mon Sep 17 00:00:00 2001 From: Matthew Piatetsky Date: Tue, 8 Jan 2019 17:05:20 -0500 Subject: [PATCH] add informational banners when masquerading as user who would not have access due to start date --- .../views/tests/test_group_configurations.py | 2 +- common/djangoapps/student/role_helpers.py | 38 ++++++++++++++++++ lms/djangoapps/courseware/access_utils.py | 28 +++++++++++-- lms/static/sass/bootstrap/_components.scss | 4 ++ lms/static/sass/elements/_banners.scss | 4 ++ openedx/core/djangoapps/util/user_messages.py | 12 +++--- .../content_type_gating/block_transformers.py | 2 +- .../content_type_gating/field_override.py | 2 +- .../features/content_type_gating/helpers.py | 39 ++----------------- .../features/content_type_gating/models.py | 3 +- .../content_type_gating/partitions.py | 6 +-- .../content_type_gating/tests/test_access.py | 7 +--- .../tests/test_partitions.py | 3 +- .../features/course_duration_limits/models.py | 4 +- .../tests/test_course_expiration.py | 3 +- 15 files changed, 95 insertions(+), 62 deletions(-) create mode 100644 common/djangoapps/student/role_helpers.py diff --git a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py index ec4fa763ca..bebface6e3 100644 --- a/cms/djangoapps/contentstore/views/tests/test_group_configurations.py +++ b/cms/djangoapps/contentstore/views/tests/test_group_configurations.py @@ -11,7 +11,7 @@ from operator import itemgetter from contentstore.utils import reverse_course_url, reverse_usage_url from contentstore.course_group_config import GroupConfiguration, CONTENT_GROUP_CONFIGURATION_NAME from contentstore.tests.utils import CourseTestCase -from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID from xmodule.partitions.partitions import Group, UserPartition, ENROLLMENT_TRACK_PARTITION_ID from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory from xmodule.validation import StudioValidation, StudioValidationMessage diff --git a/common/djangoapps/student/role_helpers.py b/common/djangoapps/student/role_helpers.py new file mode 100644 index 0000000000..373b2e9098 --- /dev/null +++ b/common/djangoapps/student/role_helpers.py @@ -0,0 +1,38 @@ +""" +Helpers for student roles +""" +from django_comment_common.models import ( + FORUM_ROLE_ADMINISTRATOR, + FORUM_ROLE_MODERATOR, + FORUM_ROLE_GROUP_MODERATOR, + FORUM_ROLE_COMMUNITY_TA, + Role +) +from student.roles import ( + CourseBetaTesterRole, + CourseInstructorRole, + CourseStaffRole, + OrgStaffRole, + OrgInstructorRole, + GlobalStaff +) + + +def has_staff_roles(user, course_key): + """ + Return true if a user has any of the following roles + Staff, Instructor, Beta Tester, Forum Community TA, Forum Group Moderator, Forum Moderator, Forum Administrator + """ + forum_roles = [FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_GROUP_MODERATOR, + FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR] + is_staff = CourseStaffRole(course_key).has_user(user) + is_instructor = CourseInstructorRole(course_key).has_user(user) + is_beta_tester = CourseBetaTesterRole(course_key).has_user(user) + is_org_staff = OrgStaffRole(course_key.org).has_user(user) + is_org_instructor = OrgInstructorRole(course_key.org).has_user(user) + is_global_staff = GlobalStaff().has_user(user) + has_forum_role = Role.user_has_role_for_course(user, course_key, forum_roles) + if any([is_staff, is_instructor, is_beta_tester, is_org_staff, + is_org_instructor, is_global_staff, has_forum_role]): + return True + return False diff --git a/lms/djangoapps/courseware/access_utils.py b/lms/djangoapps/courseware/access_utils.py index ebbb162027..fa89e3228d 100644 --- a/lms/djangoapps/courseware/access_utils.py +++ b/lms/djangoapps/courseware/access_utils.py @@ -7,11 +7,20 @@ from datetime import datetime, timedelta from logging import getLogger from django.conf import settings +from django.utils.translation import ugettext as _ from pytz import UTC from courseware.access_response import AccessResponse, StartDateError -from courseware.masquerade import get_course_masquerade, is_masquerading_as_student +from courseware.masquerade import ( + get_course_masquerade, + is_masquerading_as_specific_student, + is_masquerading_as_student +) +from crum import get_current_request +from openedx.core.djangoapps.util.user_messages import PageLevelMessages +from openedx.core.djangolib.markup import HTML from openedx.features.course_experience import COURSE_PRE_START_ACCESS_FLAG +from student.role_helpers import has_staff_roles from student.roles import CourseBetaTesterRole from xmodule.util.xmodule_django import get_current_request_hostname @@ -61,17 +70,30 @@ def check_start_date(user, days_early_for_beta, start, course_key): AccessResponse: Either ACCESS_GRANTED or StartDateError. """ start_dates_disabled = settings.FEATURES['DISABLE_START_DATES'] - if start_dates_disabled and not is_masquerading_as_student(user, course_key): + masquerading_as_student = is_masquerading_as_student(user, course_key) + masquerading_as_specific_student = is_masquerading_as_specific_student(user, course_key) + + if start_dates_disabled and not masquerading_as_student: return ACCESS_GRANTED else: now = datetime.now(UTC) - if start is None or in_preview_mode() or get_course_masquerade(user, course_key): + if start is None or in_preview_mode(): return ACCESS_GRANTED effective_start = adjust_start_date(user, days_early_for_beta, start, course_key) if now > effective_start: return ACCESS_GRANTED + if get_course_masquerade(user, course_key): + if masquerading_as_student or (masquerading_as_specific_student and not has_staff_roles(user, course_key)): + request = get_current_request() + PageLevelMessages.register_warning_message( + request, + HTML(_('This user does not have access to this content due to the content start date')), + once_only=True + ) + return ACCESS_GRANTED + return StartDateError(start) diff --git a/lms/static/sass/bootstrap/_components.scss b/lms/static/sass/bootstrap/_components.scss index c2a298b7da..3921b8e3b7 100644 --- a/lms/static/sass/bootstrap/_components.scss +++ b/lms/static/sass/bootstrap/_components.scss @@ -14,6 +14,10 @@ list-style: none; padding: 0; margin: 0; + + li { + margin: 5px 0; + } } } diff --git a/lms/static/sass/elements/_banners.scss b/lms/static/sass/elements/_banners.scss index 8a998285eb..52b59592b0 100644 --- a/lms/static/sass/elements/_banners.scss +++ b/lms/static/sass/elements/_banners.scss @@ -63,6 +63,10 @@ $full-width-banner-margin: 20px; list-style: none; padding: 0; margin: 0; + + li { + margin: 5px 0; + } } } diff --git a/openedx/core/djangoapps/util/user_messages.py b/openedx/core/djangoapps/util/user_messages.py index 3056df54f2..82b183f58d 100644 --- a/openedx/core/djangoapps/util/user_messages.py +++ b/openedx/core/djangoapps/util/user_messages.py @@ -87,7 +87,7 @@ class UserMessageCollection(): raise NotImplementedError('Subclasses must define a namespace for messages.') @classmethod - def get_message_html(self, body_html, title=None): + def get_message_html(cls, body_html, title=None, dismissable=False, **kwargs): # pylint: disable=unused-argument """ Returns the entire HTML snippet for the message. @@ -105,7 +105,7 @@ class UserMessageCollection(): return body_html @classmethod - def register_user_message(self, request, message_type, body_html, **kwargs): + def register_user_message(cls, request, message_type, body_html, once_only=False, **kwargs): """ Register a message to be shown to the user in the next page. @@ -114,10 +114,12 @@ class UserMessageCollection(): body_html (str): body of the message in html format title (str): optional title for the message as plain text dismissable (bool): shows a dismiss button (defaults to no button) + once_only (bool): show the message only once per request """ assert isinstance(message_type, UserMessageType) - message = Text(self.get_message_html(body_html, **kwargs)) - messages.add_message(request, message_type.value, Text(message), extra_tags=self.get_namespace()) + message = Text(cls.get_message_html(body_html, **kwargs)) + if not once_only or message not in [m.message for m in messages.get_messages(request)]: + messages.add_message(request, message_type.value, Text(message), extra_tags=cls.get_namespace()) @classmethod def register_info_message(self, request, message, **kwargs): @@ -184,7 +186,7 @@ class PageLevelMessages(UserMessageCollection): NAMESPACE = 'page_level_messages' @classmethod - def get_message_html(self, body_html, title=None, dismissable=False): + def get_message_html(cls, body_html, title=None, dismissable=False, **kwargs): """ Returns the entire HTML snippet for the message. """ diff --git a/openedx/features/content_type_gating/block_transformers.py b/openedx/features/content_type_gating/block_transformers.py index daf1f3f964..01527bb9e5 100644 --- a/openedx/features/content_type_gating/block_transformers.py +++ b/openedx/features/content_type_gating/block_transformers.py @@ -7,7 +7,7 @@ from django.conf import settings from openedx.core.djangoapps.content.block_structure.transformer import ( BlockStructureTransformer, ) -from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID from openedx.features.content_type_gating.models import ContentTypeGatingConfig diff --git a/openedx/features/content_type_gating/field_override.py b/openedx/features/content_type_gating/field_override.py index 76b01b4fae..88f93807e1 100644 --- a/openedx/features/content_type_gating/field_override.py +++ b/openedx/features/content_type_gating/field_override.py @@ -5,7 +5,7 @@ students in the Unlocked Group of the ContentTypeGating partition. from django.conf import settings from lms.djangoapps.courseware.field_overrides import FieldOverrideProvider -from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID from openedx.features.content_type_gating.models import ContentTypeGatingConfig diff --git a/openedx/features/content_type_gating/helpers.py b/openedx/features/content_type_gating/helpers.py index 4c42c7f323..c4b2977f1e 100644 --- a/openedx/features/content_type_gating/helpers.py +++ b/openedx/features/content_type_gating/helpers.py @@ -2,46 +2,15 @@ Helper functions used by both content_type_gating and course_duration_limits. """ -from django_comment_common.models import ( - FORUM_ROLE_ADMINISTRATOR, - FORUM_ROLE_MODERATOR, - FORUM_ROLE_GROUP_MODERATOR, - FORUM_ROLE_COMMUNITY_TA, - Role -) -from student.roles import ( - CourseBetaTesterRole, - CourseInstructorRole, - CourseStaffRole, - OrgStaffRole, - OrgInstructorRole, - GlobalStaff -) from xmodule.partitions.partitions import Group +# Studio generates partition IDs starting at 100. There is already a manually generated +# partition for Enrollment Track that uses ID 50, so we'll use 51. +CONTENT_GATING_PARTITION_ID = 51 + CONTENT_TYPE_GATE_GROUP_IDS = { 'limited_access': 1, 'full_access': 2, } LIMITED_ACCESS = Group(CONTENT_TYPE_GATE_GROUP_IDS['limited_access'], 'Limited-access Users') FULL_ACCESS = Group(CONTENT_TYPE_GATE_GROUP_IDS['full_access'], 'Full-access Users') - - -def has_staff_roles(user, course_key): - """ - Disable feature based enrollments for the enrollment if a user has any of the following roles - Staff, Instructor, Beta Tester, Forum Community TA, Forum Group Moderator, Forum Moderator, Forum Administrator - """ - forum_roles = [FORUM_ROLE_COMMUNITY_TA, FORUM_ROLE_GROUP_MODERATOR, - FORUM_ROLE_MODERATOR, FORUM_ROLE_ADMINISTRATOR] - is_staff = CourseStaffRole(course_key).has_user(user) - is_instructor = CourseInstructorRole(course_key).has_user(user) - is_beta_tester = CourseBetaTesterRole(course_key).has_user(user) - is_org_staff = OrgStaffRole(course_key.org).has_user(user) - is_org_instructor = OrgInstructorRole(course_key.org).has_user(user) - is_global_staff = GlobalStaff().has_user(user) - has_forum_role = Role.user_has_role_for_course(user, course_key, forum_roles) - if any([is_staff, is_instructor, is_beta_tester, is_org_staff, - is_org_instructor, is_global_staff, has_forum_role]): - return True - return False diff --git a/openedx/features/content_type_gating/models.py b/openedx/features/content_type_gating/models.py index 1d0a2eec6a..a3a3075827 100644 --- a/openedx/features/content_type_gating/models.py +++ b/openedx/features/content_type_gating/models.py @@ -20,12 +20,13 @@ from lms.djangoapps.courseware.masquerade import ( ) from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel from openedx.core.djangoapps.config_model_utils.utils import is_in_holdback -from openedx.features.content_type_gating.helpers import FULL_ACCESS, has_staff_roles, LIMITED_ACCESS +from openedx.features.content_type_gating.helpers import FULL_ACCESS, LIMITED_ACCESS from openedx.features.course_duration_limits.config import ( CONTENT_TYPE_GATING_FLAG, FEATURE_BASED_ENROLLMENT_GLOBAL_KILL_FLAG, ) from student.models import CourseEnrollment +from student.role_helpers import has_staff_roles from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID diff --git a/openedx/features/content_type_gating/partitions.py b/openedx/features/content_type_gating/partitions.py index effdc10443..41eeeeef1c 100644 --- a/openedx/features/content_type_gating/partitions.py +++ b/openedx/features/content_type_gating/partitions.py @@ -18,14 +18,10 @@ from lms.djangoapps.commerce.utils import EcommerceService from xmodule.partitions.partitions import UserPartition, UserPartitionError from openedx.core.lib.mobile_utils import is_request_from_mobile_app from openedx.features.content_type_gating.models import ContentTypeGatingConfig -from openedx.features.content_type_gating.helpers import FULL_ACCESS, LIMITED_ACCESS +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, FULL_ACCESS, LIMITED_ACCESS LOG = logging.getLogger(__name__) -# Studio generates partition IDs starting at 100. There is already a manually generated -# partition for Enrollment Track that uses ID 50, so we'll use 51. -CONTENT_GATING_PARTITION_ID = 51 - def create_content_gating_partition(course): """ diff --git a/openedx/features/content_type_gating/tests/test_access.py b/openedx/features/content_type_gating/tests/test_access.py index 7052c361fd..6cbf1b3314 100644 --- a/openedx/features/content_type_gating/tests/test_access.py +++ b/openedx/features/content_type_gating/tests/test_access.py @@ -35,11 +35,8 @@ from lms.djangoapps.courseware.tests.factories import ( from openedx.core.djangoapps.user_api.tests.factories import UserCourseTagFactory from openedx.core.djangoapps.util.testing import TestConditionalContent from openedx.core.lib.url_utils import quote_slashes -from openedx.features.content_type_gating.helpers import CONTENT_TYPE_GATE_GROUP_IDS -from openedx.features.content_type_gating.partitions import ( - CONTENT_GATING_PARTITION_ID, - ContentTypeGatingPartition -) +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS +from openedx.features.content_type_gating.partitions import ContentTypeGatingPartition from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.course_duration_limits.config import ( EXPERIMENT_ID, diff --git a/openedx/features/content_type_gating/tests/test_partitions.py b/openedx/features/content_type_gating/tests/test_partitions.py index a4cbaf526b..9cbf4af8ff 100644 --- a/openedx/features/content_type_gating/tests/test_partitions.py +++ b/openedx/features/content_type_gating/tests/test_partitions.py @@ -3,7 +3,8 @@ from mock import Mock, patch from opaque_keys.edx.keys import CourseKey from openedx.core.djangolib.testing.utils import CacheIsolationTestCase -from openedx.features.content_type_gating.partitions import create_content_gating_partition, CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.partitions import create_content_gating_partition from openedx.features.content_type_gating.models import ContentTypeGatingConfig from xmodule.partitions.partitions import UserPartitionError diff --git a/openedx/features/course_duration_limits/models.py b/openedx/features/course_duration_limits/models.py index 7367852a80..9a74ba4a02 100644 --- a/openedx/features/course_duration_limits/models.py +++ b/openedx/features/course_duration_limits/models.py @@ -20,13 +20,13 @@ from lms.djangoapps.courseware.masquerade import ( ) from openedx.core.djangoapps.config_model_utils.models import StackedConfigurationModel from openedx.core.djangoapps.config_model_utils.utils import is_in_holdback -from openedx.features.content_type_gating.helpers import CONTENT_TYPE_GATE_GROUP_IDS, has_staff_roles -from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS from openedx.features.course_duration_limits.config import ( CONTENT_TYPE_GATING_FLAG, FEATURE_BASED_ENROLLMENT_GLOBAL_KILL_FLAG, ) from student.models import CourseEnrollment +from student.role_helpers import has_staff_roles from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID diff --git a/openedx/features/course_duration_limits/tests/test_course_expiration.py b/openedx/features/course_duration_limits/tests/test_course_expiration.py index 3a7e0e91d5..a87abe0825 100644 --- a/openedx/features/course_duration_limits/tests/test_course_expiration.py +++ b/openedx/features/course_duration_limits/tests/test_course_expiration.py @@ -29,8 +29,7 @@ from lms.djangoapps.courseware.tests.factories import ( ) from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory -from openedx.features.content_type_gating.helpers import CONTENT_TYPE_GATE_GROUP_IDS -from openedx.features.content_type_gating.partitions import CONTENT_GATING_PARTITION_ID +from openedx.features.content_type_gating.helpers import CONTENT_GATING_PARTITION_ID, CONTENT_TYPE_GATE_GROUP_IDS from openedx.features.course_duration_limits.access import get_user_course_expiration_date, MIN_DURATION, MAX_DURATION from openedx.features.course_duration_limits.config import EXPERIMENT_ID, EXPERIMENT_DATA_HOLDBACK_KEY from openedx.features.course_experience.tests.views.helpers import add_course_mode