From f2fecde007debe91e2018ef7c876a4a1a891b6a4 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Sun, 12 Dec 2021 02:31:01 +0700 Subject: [PATCH 001/519] [CHANGE] Show unit block in OutlineTabView API --- lms/djangoapps/course_home_api/outline/serializers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/course_home_api/outline/serializers.py b/lms/djangoapps/course_home_api/outline/serializers.py index 63c059c160cb..175d05f5abf6 100644 --- a/lms/djangoapps/course_home_api/outline/serializers.py +++ b/lms/djangoapps/course_home_api/outline/serializers.py @@ -19,7 +19,8 @@ class CourseBlockSerializer(serializers.Serializer): def get_blocks(self, block): # pylint: disable=missing-function-docstring block_key = block['id'] block_type = block['type'] - children = block.get('children', []) if block_type != 'sequential' else [] # Don't descend past sequential + # Allow sequential children + children = block.get('children', []) if block_type != 'vertical' else [] # Don't descend past vertical description = block.get('format') display_name = block['display_name'] enable_links = self.context.get('enable_links') From 971ebfb174fb1d71a84040e981621d63f7465f97 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 20 Dec 2021 16:49:25 +0000 Subject: [PATCH 002/519] [ADD] Add extract lesson time by regex --- .../effort_estimation/block_transformers.py | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/openedx/features/effort_estimation/block_transformers.py b/openedx/features/effort_estimation/block_transformers.py index 11efd6fba702..5a2dcfd7d896 100644 --- a/openedx/features/effort_estimation/block_transformers.py +++ b/openedx/features/effort_estimation/block_transformers.py @@ -4,6 +4,7 @@ """ import math +import re import crum import lxml.html @@ -11,6 +12,7 @@ from edxval.api import get_videos_for_course from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer +from openedx.core.djangoapps.content.block_structure.block_structure import BlockStructureModulestoreData from openedx.core.lib.mobile_utils import is_request_from_mobile_app from .toggles import EFFORT_ESTIMATION_DISABLED_FLAG @@ -42,8 +44,10 @@ class EffortEstimationTransformer(BlockStructureTransformer): # Private transformer field names DISABLE_ESTIMATION = 'disable_estimation' HTML_WORD_COUNT = 'html_word_count' + TIME_BY_REGEX = 'time_by_regex' VIDEO_CLIP_DURATION = 'video_clip_duration' VIDEO_DURATION = 'video_duration' + TIME_EFFORT_REGEX = r"[0-9]+" CACHE_VIDEO_DURATIONS = 'video.durations' DEFAULT_WPM = 265 # words per minute @@ -97,6 +101,17 @@ def _collect_html_effort(cls, block_structure, block_key, xblock, _cache): block_structure.set_transformer_block_field(block_key, cls, cls.HTML_WORD_COUNT, len(text.split())) + lines = text.strip().split("\n") + time_by_regex = None + + if len(lines) > 0: + last_line = lines[-1] + regex_result = re.findall(cls.TIME_EFFORT_REGEX, last_line) + + time_by_regex = int(regex_result[-1]) if len(regex_result) > 0 else None + + block_structure.set_transformer_block_field(block_key, cls, cls.TIME_BY_REGEX, time_by_regex) + @classmethod def _collect_video_effort(cls, block_structure, block_key, xblock, cache): """Records a duration for later viewing speed calculations.""" @@ -135,7 +150,7 @@ def transform(self, usage_info, block_structure): 'chapter': self._estimate_children_effort, 'course': self._estimate_children_effort, 'html': self._estimate_html_effort, - 'sequential': self._estimate_children_effort, + 'sequential': self._estimate_sequential_effort_by_regex, 'vertical': self._estimate_vertical_effort, 'video': self._estimate_video_effort, } @@ -219,3 +234,17 @@ def _estimate_video_effort(self, _usage_info, block_structure, block_key): # We are intentionally only looking at global_speed, not speed (which is last speed user used on this video) # because this estimate is meant to be somewhat static. return user_duration / global_speed, 0 + + def _estimate_sequential_effort_by_regex(self, _usage_info, block_structure, block_key): + """Returns an expected time to view the video, at the user's preferred speed.""" + time = None + cls = EffortEstimationTransformer + vertical_childs = block_structure.get_children(block_key) + if len(vertical_childs) > 0: + first_html = block_structure.get_children(vertical_childs[0])[0] + time = block_structure.get_transformer_block_field(first_html, cls, self.TIME_BY_REGEX) + + activities = self._gather_child_values(block_structure, block_key, self.EFFORT_ACTIVITIES, default=1) + time = time * 60 if time is not None else None + + return time, activities From d70573d47b3825cb86da90c16e8687172dc32c5d Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Sun, 26 Dec 2021 14:57:35 +0000 Subject: [PATCH 003/519] [CHANGE] Change logic to extract time by regex --- .../effort_estimation/block_transformers.py | 13 +---- .../estimate_regex/estimate_time_by_regex.py | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py diff --git a/openedx/features/effort_estimation/block_transformers.py b/openedx/features/effort_estimation/block_transformers.py index 5a2dcfd7d896..15bdc54ed9ff 100644 --- a/openedx/features/effort_estimation/block_transformers.py +++ b/openedx/features/effort_estimation/block_transformers.py @@ -4,7 +4,6 @@ """ import math -import re import crum import lxml.html @@ -14,6 +13,7 @@ from openedx.core.djangoapps.content.block_structure.transformer import BlockStructureTransformer from openedx.core.djangoapps.content.block_structure.block_structure import BlockStructureModulestoreData from openedx.core.lib.mobile_utils import is_request_from_mobile_app +from openedx.features.effort_estimation.estimate_regex.estimate_time_by_regex import estimate_time_by_regex from .toggles import EFFORT_ESTIMATION_DISABLED_FLAG @@ -47,7 +47,6 @@ class EffortEstimationTransformer(BlockStructureTransformer): TIME_BY_REGEX = 'time_by_regex' VIDEO_CLIP_DURATION = 'video_clip_duration' VIDEO_DURATION = 'video_duration' - TIME_EFFORT_REGEX = r"[0-9]+" CACHE_VIDEO_DURATIONS = 'video.durations' DEFAULT_WPM = 265 # words per minute @@ -101,15 +100,7 @@ def _collect_html_effort(cls, block_structure, block_key, xblock, _cache): block_structure.set_transformer_block_field(block_key, cls, cls.HTML_WORD_COUNT, len(text.split())) - lines = text.strip().split("\n") - time_by_regex = None - - if len(lines) > 0: - last_line = lines[-1] - regex_result = re.findall(cls.TIME_EFFORT_REGEX, last_line) - - time_by_regex = int(regex_result[-1]) if len(regex_result) > 0 else None - + time_by_regex = estimate_time_by_regex(text) block_structure.set_transformer_block_field(block_key, cls, cls.TIME_BY_REGEX, time_by_regex) @classmethod diff --git a/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py b/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py new file mode 100644 index 000000000000..22d68b68f941 --- /dev/null +++ b/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py @@ -0,0 +1,58 @@ +""" +Estimate time effort using regex to extract time data from text +""" + +import re +import math + +TIME_HOUR_REGEX = r"([0-9]+\.*[0-9]*) giờ" +TIME_MINUTE_REGEX = r"([0-9]+) phút" + +def _get_hour_time(line): + """ + Get hour from string + """ + hour = 0 + + regex_result = re.findall(TIME_HOUR_REGEX, line) + if len(regex_result) > 0: + hour = float(regex_result[-1]) + + return hour + +def _get_minute_time(line): + """ + Get minute from string + """ + minute = 0 + + regex_result = re.findall(TIME_MINUTE_REGEX, line) + if len(regex_result) > 0: + minute = int(regex_result[-1]) + + return minute + +def estimate_time_by_regex(text): + """Estimate time effort using regex to extract time data from text + + Args: + text (string): Content to extract + + Returns: + int: The time effort in minute (round by 5 min), if can't extract will return None + """ + lines = text.strip().split("\n") + if len(lines) == 0: + return None + + time_by_regex = None + last_line = lines[-1].lower() + + hour = _get_hour_time(last_line) + minute = _get_minute_time(last_line) + + if hour != 0 or minute != 0: + time_by_regex = math.ceil(hour * 60) + minute + time_by_regex = math.ceil(time_by_regex / 5) * 5 + + return time_by_regex From 68f76bc645e5bdf30b3dc2c40d276f45fd2f99af Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 27 Dec 2021 03:38:34 +0000 Subject: [PATCH 004/519] [ADD] Init funix_relative_date app --- cms/envs/common.py | 1 + lms/envs/common.py | 1 + openedx/features/funix_relative_date/apps.py | 15 ++++++++++++ .../funix_relative_date.py | 14 +++++++++++ .../features/funix_relative_date/handlers.py | 23 +++++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100644 openedx/features/funix_relative_date/apps.py create mode 100644 openedx/features/funix_relative_date/funix_relative_date.py create mode 100644 openedx/features/funix_relative_date/handlers.py diff --git a/cms/envs/common.py b/cms/envs/common.py index 6f4ab636a4ca..28d57962d879 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1661,6 +1661,7 @@ 'openedx.features.content_type_gating', 'openedx.features.discounts', 'openedx.features.effort_estimation', + 'openedx.features.funix_relative_date', 'lms.djangoapps.experiments', 'openedx.core.djangoapps.external_user_ids', diff --git a/lms/envs/common.py b/lms/envs/common.py index 53672981b2e1..7f3539254897 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3143,6 +3143,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring 'openedx.features.content_type_gating', 'openedx.features.discounts', 'openedx.features.effort_estimation', + 'openedx.features.funix_relative_date', 'openedx.features.name_affirmation_api.apps.NameAffirmationApiConfig', 'lms.djangoapps.experiments', diff --git a/openedx/features/funix_relative_date/apps.py b/openedx/features/funix_relative_date/apps.py new file mode 100644 index 000000000000..5b1d5a1bce53 --- /dev/null +++ b/openedx/features/funix_relative_date/apps.py @@ -0,0 +1,15 @@ +""" +Define the funix_relative_date Django App. +""" +from django.apps import AppConfig + +class FunixRelativeDateConfig(AppConfig): + """ + Application Configuration for FunixRelativeDate. + """ + name = 'openedx.features.funix_relative_date' + def ready(self): + """ + Connect signal handlers. + """ + from . import handlers # pylint: disable=unused-import diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py new file mode 100644 index 000000000000..7dd6c37cbcc0 --- /dev/null +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -0,0 +1,14 @@ +from lms.djangoapps.courseware.courses import get_course_assignment_date_blocks, get_course_with_access +from common.djangoapps.student.models import get_user_by_username_or_email +from opaque_keys.edx.keys import CourseKey + +class FunixRelativeDate(): + @classmethod + def get_schedule(self, user_name, course_id): + user = get_user_by_username_or_email(user_name) + course_key = CourseKey.from_string(course_id) + course = get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) + + # courses = get_course_assignment_date_blocks(course=course, user=user, request=None, include_access=True, include_past_dates=True) + + # print(courses) diff --git a/openedx/features/funix_relative_date/handlers.py b/openedx/features/funix_relative_date/handlers.py new file mode 100644 index 000000000000..addb430e808c --- /dev/null +++ b/openedx/features/funix_relative_date/handlers.py @@ -0,0 +1,23 @@ +""" +Badges related signal handlers. +""" +from django.dispatch import receiver + +from common.djangoapps.student.models import EnrollStatusChange +from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE +from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDate +from xmodule.modulestore.django import SignalHandler # lint-amnesty, pylint: disable=wrong-import-order + + +@receiver(ENROLL_STATUS_CHANGE) +def handle_user_enroll(sender, event=None, user=None, course_id=None,**kwargs): # pylint: disable=unused-argument + """ + Awards enrollment badge to the given user on new enrollments. + """ + if event == EnrollStatusChange.enroll: + pass + +@receiver(SignalHandler.course_published) +def listen_for_course_publish(sender, course_key, **kwargs): + print('lkj=dddfd') + FunixRelativeDate.get_schedule(user_name='edx',course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') From a5fbd296cad0a3a20a0f49a4207cdbb86e4ab624 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 27 Dec 2021 06:44:58 +0000 Subject: [PATCH 005/519] [ADD] Get course grade assignment --- lms/djangoapps/courseware/courses.py | 58 +++++++++++++++++++ .../funix_relative_date.py | 7 +-- .../features/funix_relative_date/handlers.py | 1 - 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 9d7575e007ab..de64096bc672 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -915,3 +915,61 @@ def get_course_chapter_ids(course_key): log.exception('Failed to retrieve course from modulestore.') return [] return [str(chapter_key) for chapter_key in chapter_keys if chapter_key.block_type == 'chapter'] + +def get_course_granded_lesson(course_key, user, include_access=False): + if not user.id: + return [] + store = modulestore() + course_usage_key = store.make_course_usage_key(course_key) + block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True) + + now = datetime.now(pytz.UTC) + assignments = [] + for section_key in block_data.get_children(course_usage_key): # lint-amnesty, pylint: disable=too-many-nested-blocks + for subsection_key in block_data.get_children(section_key): + due = block_data.get_xblock_field(subsection_key, 'due', None) + graded = block_data.get_xblock_field(subsection_key, 'graded', False) + if graded: + first_component_block_id = get_first_component_of_block(subsection_key, block_data) + contains_gated_content = include_access and block_data.get_xblock_field( + subsection_key, 'contains_gated_content', False) + title = block_data.get_xblock_field(subsection_key, 'display_name', _('Assignment')) + + assignment_type = block_data.get_xblock_field(subsection_key, 'format', None) + + url = None + start = block_data.get_xblock_field(subsection_key, 'start') + assignment_released = not start or start < now + if assignment_released: + # url = reverse('jump_to', args=[course_key, subsection_key]) + complete = is_block_structure_complete_for_assignments(block_data, subsection_key) + else: + complete = False + + # past_due = not complete and due < now + past_due = False + assignments.append(_Assignment( + subsection_key, title, url, due, contains_gated_content, + complete, past_due, assignment_type, None, first_component_block_id + )) + + return assignments + +def funix_get_assginment_date_blocks(course, user, request, num_return=None, include_past_dates=True): + date_blocks = [] + for assignment in get_course_granded_lesson(course.id, user, include_access=True): + date_block = CourseAssignmentDate(course, user) + date_block.date = assignment.date + date_block.contains_gated_content = assignment.contains_gated_content + date_block.first_component_block_id = assignment.first_component_block_id + date_block.complete = assignment.complete + date_block.assignment_type = assignment.assignment_type + date_block.past_due = assignment.past_due + # date_block.link = request.build_absolute_uri(assignment.url) if assignment.url else '' + date_block.set_title(assignment.title, link=assignment.url) + date_block._extra_info = assignment.extra_info # pylint: disable=protected-access + date_blocks.append(date_block) + date_blocks = sorted((b for b in date_blocks if (b.is_enabled or include_past_dates)), key=date_block_key_fn) + if num_return: + return date_blocks[:num_return] + return date_blocks diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 7dd6c37cbcc0..917d4b93e327 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -1,4 +1,4 @@ -from lms.djangoapps.courseware.courses import get_course_assignment_date_blocks, get_course_with_access +from lms.djangoapps.courseware.courses import funix_get_assginment_date_blocks, get_course_with_access from common.djangoapps.student.models import get_user_by_username_or_email from opaque_keys.edx.keys import CourseKey @@ -8,7 +8,4 @@ def get_schedule(self, user_name, course_id): user = get_user_by_username_or_email(user_name) course_key = CourseKey.from_string(course_id) course = get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) - - # courses = get_course_assignment_date_blocks(course=course, user=user, request=None, include_access=True, include_past_dates=True) - - # print(courses) + courses = funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) diff --git a/openedx/features/funix_relative_date/handlers.py b/openedx/features/funix_relative_date/handlers.py index addb430e808c..93b80a984309 100644 --- a/openedx/features/funix_relative_date/handlers.py +++ b/openedx/features/funix_relative_date/handlers.py @@ -19,5 +19,4 @@ def handle_user_enroll(sender, event=None, user=None, course_id=None,**kwargs): @receiver(SignalHandler.course_published) def listen_for_course_publish(sender, course_key, **kwargs): - print('lkj=dddfd') FunixRelativeDate.get_schedule(user_name='edx',course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') From 94989378d8716943c52f8c2cb73db1be7466a1de Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 27 Dec 2021 15:01:54 +0000 Subject: [PATCH 006/519] [ADD] create funix_relative_date app --- .../features/funix_relative_date/__init__.py | 0 openedx/features/funix_relative_date/admin.py | 8 ++++++ .../features/funix_relative_date/handlers.py | 5 +--- .../migrations/0001_initial.py | 25 +++++++++++++++++++ .../migrations/__init__.py | 0 .../features/funix_relative_date/models.py | 11 ++++++++ 6 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 openedx/features/funix_relative_date/__init__.py create mode 100644 openedx/features/funix_relative_date/admin.py create mode 100644 openedx/features/funix_relative_date/migrations/0001_initial.py create mode 100644 openedx/features/funix_relative_date/migrations/__init__.py create mode 100644 openedx/features/funix_relative_date/models.py diff --git a/openedx/features/funix_relative_date/__init__.py b/openedx/features/funix_relative_date/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/funix_relative_date/admin.py b/openedx/features/funix_relative_date/admin.py new file mode 100644 index 000000000000..301858a3c3b2 --- /dev/null +++ b/openedx/features/funix_relative_date/admin.py @@ -0,0 +1,8 @@ +from config_models.admin import ConfigurationModelAdmin +from django.contrib import admin + +from openedx.features.funix_relative_date.models import ( + FunixRelativeDate +) + +admin.site.register(FunixRelativeDate) diff --git a/openedx/features/funix_relative_date/handlers.py b/openedx/features/funix_relative_date/handlers.py index 93b80a984309..6d818e2a04fd 100644 --- a/openedx/features/funix_relative_date/handlers.py +++ b/openedx/features/funix_relative_date/handlers.py @@ -1,5 +1,5 @@ """ -Badges related signal handlers. +FunixRelativeDate related signal handlers. """ from django.dispatch import receiver @@ -11,9 +11,6 @@ @receiver(ENROLL_STATUS_CHANGE) def handle_user_enroll(sender, event=None, user=None, course_id=None,**kwargs): # pylint: disable=unused-argument - """ - Awards enrollment badge to the given user on new enrollments. - """ if event == EnrollStatusChange.enroll: pass diff --git a/openedx/features/funix_relative_date/migrations/0001_initial.py b/openedx/features/funix_relative_date/migrations/0001_initial.py new file mode 100644 index 000000000000..2cbe11caafed --- /dev/null +++ b/openedx/features/funix_relative_date/migrations/0001_initial.py @@ -0,0 +1,25 @@ +# Generated by Django 3.2.10 on 2021-12-27 11:09 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='FunixRelativeDate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user_id', models.CharField(max_length=255)), + ('course_id', models.CharField(max_length=255)), + ('block_id', models.EmailField(max_length=255)), + ('type', models.EmailField(max_length=255)), + ('index', models.IntegerField()), + ], + ), + ] diff --git a/openedx/features/funix_relative_date/migrations/__init__.py b/openedx/features/funix_relative_date/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py new file mode 100644 index 000000000000..854b9eae4ce5 --- /dev/null +++ b/openedx/features/funix_relative_date/models.py @@ -0,0 +1,11 @@ +from django.db import models + +class FunixRelativeDate(models.Model): + user_id = models.CharField(max_length=255) + course_id = models.CharField(max_length=255) + block_id = models.EmailField(max_length=255) + type = models.EmailField(max_length=255) + index = models.IntegerField() + + def __str__(self): + return "%s %s" % (self.user_id, self.course_id) From 5c95e084daa0e74d51eef7b020ea0a09cbd533f8 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 27 Dec 2021 18:35:33 +0000 Subject: [PATCH 007/519] [ADD] Get last_complete_date --- lms/djangoapps/course_blocks/api.py | 6 +++++ lms/djangoapps/courseware/courses.py | 6 ++++- lms/djangoapps/courseware/date_summary.py | 1 + .../funix_relative_date.py | 18 +++++++++++++-- .../features/funix_relative_date/handlers.py | 10 +++++--- .../0002_alter_funixrelativedate_block_id.py | 18 +++++++++++++++ .../migrations/0003_funixrelativedate_date.py | 19 +++++++++++++++ .../migrations/0004_auto_20211227_1830.py | 23 +++++++++++++++++++ .../features/funix_relative_date/models.py | 18 ++++++++++++--- 9 files changed, 110 insertions(+), 9 deletions(-) create mode 100644 openedx/features/funix_relative_date/migrations/0002_alter_funixrelativedate_block_id.py create mode 100644 openedx/features/funix_relative_date/migrations/0003_funixrelativedate_date.py create mode 100644 openedx/features/funix_relative_date/migrations/0004_auto_20211227_1830.py diff --git a/lms/djangoapps/course_blocks/api.py b/lms/djangoapps/course_blocks/api.py index c978f54e02d5..f8c99739fe36 100644 --- a/lms/djangoapps/course_blocks/api.py +++ b/lms/djangoapps/course_blocks/api.py @@ -8,6 +8,7 @@ from edx_when import field_data from lms.djangoapps.course_api.blocks.transformers.block_completion import BlockCompletionTransformer +from openedx.features.effort_estimation.block_transformers import EffortEstimationTransformer from openedx.core.djangoapps.content.block_structure.api import get_block_structure_manager from openedx.core.djangoapps.content.block_structure.transformers import BlockStructureTransformers from openedx.features.content_type_gating.block_transformers import ContentTypeGateTransformer @@ -62,6 +63,7 @@ def get_course_blocks( allow_start_dates_in_future=False, include_completion=False, include_has_scheduled_content=False, + include_effort_estimation=False, ): """ A higher order function implemented on top of the @@ -97,6 +99,10 @@ def get_course_blocks( transformers = BlockStructureTransformers(get_course_block_access_transformers(user)) if include_completion: transformers += [BlockCompletionTransformer()] + + if include_effort_estimation: + transformers += [EffortEstimationTransformer()] + transformers.usage_info = CourseUsageInfo( starting_block_usage_key.course_key, user, diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index de64096bc672..1e09679eff80 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -921,7 +921,7 @@ def get_course_granded_lesson(course_key, user, include_access=False): return [] store = modulestore() course_usage_key = store.make_course_usage_key(course_key) - block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True) + block_data = get_course_blocks(user, course_usage_key, allow_start_dates_in_future=True, include_completion=True, include_effort_estimation=True) now = datetime.now(pytz.UTC) assignments = [] @@ -930,6 +930,9 @@ def get_course_granded_lesson(course_key, user, include_access=False): due = block_data.get_xblock_field(subsection_key, 'due', None) graded = block_data.get_xblock_field(subsection_key, 'graded', False) if graded: + effort_time = block_data.get_xblock_field(subsection_key, 'effort_time', -1) + print('--sdsd---') + print(effort_time) first_component_block_id = get_first_component_of_block(subsection_key, block_data) contains_gated_content = include_access and block_data.get_xblock_field( subsection_key, 'contains_gated_content', False) @@ -965,6 +968,7 @@ def funix_get_assginment_date_blocks(course, user, request, num_return=None, inc date_block.complete = assignment.complete date_block.assignment_type = assignment.assignment_type date_block.past_due = assignment.past_due + date_block.block_key = assignment.block_key # date_block.link = request.build_absolute_uri(assignment.url) if assignment.url else '' date_block.set_title(assignment.title, link=assignment.url) date_block._extra_info = assignment.extra_info # pylint: disable=protected-access diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index f90fa17dccad..7cce2d18114a 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -409,6 +409,7 @@ def __init__(self, *args, **kwargs): self.complete = None self.past_due = None self._extra_info = None + self.block_key = None @property def date(self): diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 917d4b93e327..e9520ac299f2 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -1,11 +1,25 @@ from lms.djangoapps.courseware.courses import funix_get_assginment_date_blocks, get_course_with_access from common.djangoapps.student.models import get_user_by_username_or_email +from openedx.features.funix_relative_date.models import FunixRelativeDateDAO from opaque_keys.edx.keys import CourseKey -class FunixRelativeDate(): +class FunixRelativeDateLibary(): + @classmethod + def _get_last_complete_assignment(self, assignment_blocks): + return next((asm for asm in assignment_blocks[::-1] if asm.complete), None) + + @classmethod def get_schedule(self, user_name, course_id): user = get_user_by_username_or_email(user_name) course_key = CourseKey.from_string(course_id) course = get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) - courses = funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) + assignment_blocks = funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) + + last_complete = self._get_last_complete_assignment(assignment_blocks=assignment_blocks) + + last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id) + if last_complete is not None: + last_complete_date = FunixRelativeDateDAO.get_block_by_id(user_id=user.id, course_id=course_id, block_id=last_complete.subsection_key) + + print(last_complete_date) diff --git a/openedx/features/funix_relative_date/handlers.py b/openedx/features/funix_relative_date/handlers.py index 6d818e2a04fd..a0ab3f90163b 100644 --- a/openedx/features/funix_relative_date/handlers.py +++ b/openedx/features/funix_relative_date/handlers.py @@ -5,15 +5,19 @@ from common.djangoapps.student.models import EnrollStatusChange from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE -from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDate +from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary +from openedx.features.funix_relative_date.models import FunixRelativeDate from xmodule.modulestore.django import SignalHandler # lint-amnesty, pylint: disable=wrong-import-order @receiver(ENROLL_STATUS_CHANGE) def handle_user_enroll(sender, event=None, user=None, course_id=None,**kwargs): # pylint: disable=unused-argument if event == EnrollStatusChange.enroll: - pass + # Add user enrollment + enrollment = FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=None, type='start', index=0) + enrollment.save() + @receiver(SignalHandler.course_published) def listen_for_course_publish(sender, course_key, **kwargs): - FunixRelativeDate.get_schedule(user_name='edx',course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') + FunixRelativeDateLibary.get_schedule(user_name='edx',course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') diff --git a/openedx/features/funix_relative_date/migrations/0002_alter_funixrelativedate_block_id.py b/openedx/features/funix_relative_date/migrations/0002_alter_funixrelativedate_block_id.py new file mode 100644 index 000000000000..daa3af0cff77 --- /dev/null +++ b/openedx/features/funix_relative_date/migrations/0002_alter_funixrelativedate_block_id.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.10 on 2021-12-27 16:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funix_relative_date', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='funixrelativedate', + name='block_id', + field=models.EmailField(max_length=255, null=True), + ), + ] diff --git a/openedx/features/funix_relative_date/migrations/0003_funixrelativedate_date.py b/openedx/features/funix_relative_date/migrations/0003_funixrelativedate_date.py new file mode 100644 index 000000000000..2b0e38b78038 --- /dev/null +++ b/openedx/features/funix_relative_date/migrations/0003_funixrelativedate_date.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.10 on 2021-12-27 18:27 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('funix_relative_date', '0002_alter_funixrelativedate_block_id'), + ] + + operations = [ + migrations.AddField( + model_name='funixrelativedate', + name='date', + field=models.DateField(default=django.utils.timezone.now, editable=False), + ), + ] diff --git a/openedx/features/funix_relative_date/migrations/0004_auto_20211227_1830.py b/openedx/features/funix_relative_date/migrations/0004_auto_20211227_1830.py new file mode 100644 index 000000000000..2bb7ec46d07c --- /dev/null +++ b/openedx/features/funix_relative_date/migrations/0004_auto_20211227_1830.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.10 on 2021-12-27 18:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('funix_relative_date', '0003_funixrelativedate_date'), + ] + + operations = [ + migrations.AlterField( + model_name='funixrelativedate', + name='block_id', + field=models.CharField(max_length=255, null=True), + ), + migrations.AlterField( + model_name='funixrelativedate', + name='type', + field=models.CharField(max_length=255), + ), + ] diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py index 854b9eae4ce5..688ae06417b3 100644 --- a/openedx/features/funix_relative_date/models.py +++ b/openedx/features/funix_relative_date/models.py @@ -1,11 +1,23 @@ from django.db import models +from django.utils.timezone import now class FunixRelativeDate(models.Model): user_id = models.CharField(max_length=255) course_id = models.CharField(max_length=255) - block_id = models.EmailField(max_length=255) - type = models.EmailField(max_length=255) + block_id = models.CharField(max_length=255, null=True) + type = models.CharField(max_length=255) + date = models.DateField(default=now, editable=False) index = models.IntegerField() def __str__(self): - return "%s %s" % (self.user_id, self.course_id) + return "%s %s %s" % (self.user_id, self.course_id, self.block_id) + +class FunixRelativeDateDAO(): + @classmethod + def get_block_by_id(self, user_id, course_id, block_id): + return FunixRelativeDate.objects.get(user_id=user_id, course_id=course_id, block_id=block_id) + + @classmethod + def get_enroll_by_id(self, user_id, course_id): + print('43-23', user_id, course_id) + return FunixRelativeDate.objects.get(user_id=user_id, course_id=course_id, type="start") From 1e172ae50ea519cfba6debbdfdf374b3f237fdda Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 28 Dec 2021 08:53:24 +0000 Subject: [PATCH 008/519] [ADD] Add complete_date in BlockCompletionTransformer --- .../blocks/transformers/block_completion.py | 25 +++++++++++++++++-- lms/djangoapps/courseware/courses.py | 13 +++++++--- lms/djangoapps/courseware/date_summary.py | 1 + .../funix_relative_date.py | 6 ++--- 4 files changed, 35 insertions(+), 10 deletions(-) diff --git a/lms/djangoapps/course_api/blocks/transformers/block_completion.py b/lms/djangoapps/course_api/blocks/transformers/block_completion.py index 472555c4c7f9..6d0bd5cb3150 100644 --- a/lms/djangoapps/course_api/blocks/transformers/block_completion.py +++ b/lms/djangoapps/course_api/blocks/transformers/block_completion.py @@ -17,6 +17,7 @@ class BlockCompletionTransformer(BlockStructureTransformer): WRITE_VERSION = 1 COMPLETION = 'completion' COMPLETE = 'complete' + COMPLETE_TIME = 'complete_time' RESUME_BLOCK = 'resume_block' @classmethod @@ -56,7 +57,19 @@ def _is_block_excluded(block_structure, block_key): return completion_mode == CompletionMode.EXCLUDED - def mark_complete(self, complete_course_blocks, latest_complete_block_key, block_key, block_structure): + def _get_complete_time(self, child_blocks, block_structure): + return max([block_structure.get_xblock_field(child_key, self.COMPLETE_TIME, None) for child_key in child_blocks]) + + def _get_complete_time_leaf_block(self, block_key, usage_info): + complete_block = BlockCompletion.objects.get( + user=usage_info.user, + context_key=usage_info.course_key, + block_key=block_key + ) + return complete_block.modified + + + def mark_complete(self, complete_course_blocks, latest_complete_block_key, block_key, block_structure, usage_info): """ Helper function to mark a block as 'complete' as dictated by complete_course_blocks (for problems) or all of a block's children being complete. @@ -69,6 +82,10 @@ def mark_complete(self, complete_course_blocks, latest_complete_block_key, block """ if block_key in complete_course_blocks: block_structure.override_xblock_field(block_key, self.COMPLETE, True) + + complete_time = self._get_complete_time_leaf_block(block_key, usage_info) + block_structure.override_xblock_field(block_key, self.COMPLETE_TIME, complete_time) + if str(block_key) == str(latest_complete_block_key): block_structure.override_xblock_field(block_key, self.RESUME_BLOCK, True) elif block_structure.get_xblock_field(block_key, 'completion_mode') == CompletionMode.AGGREGATOR: @@ -80,6 +97,10 @@ def mark_complete(self, complete_course_blocks, latest_complete_block_key, block if all_children_complete: block_structure.override_xblock_field(block_key, self.COMPLETE, True) + if len(children) > 0: + complete_time = self._get_complete_time(children, block_structure) + block_structure.override_xblock_field(block_key, self.COMPLETE_TIME, complete_time) + if any(block_structure.get_xblock_field(child_key, self.RESUME_BLOCK) for child_key in children): block_structure.override_xblock_field(block_key, self.RESUME_BLOCK, True) @@ -138,4 +159,4 @@ def _is_block_an_aggregator_or_excluded(block_key): if latest_complete_key: complete_keys = {key for key, completion in completions_dict.items() if completion == 1.0} for block_key in block_structure.post_order_traversal(): - self.mark_complete(complete_keys, latest_complete_key, block_key, block_structure) + self.mark_complete(complete_keys, latest_complete_key, block_key, block_structure, usage_info) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 1e09679eff80..b0c4aa068c7b 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -65,6 +65,7 @@ from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.x_module import STUDENT_VIEW # lint-amnesty, pylint: disable=wrong-import-order +from lms.djangoapps.course_api.blocks.transformers.block_completion import BlockCompletionTransformer log = logging.getLogger(__name__) @@ -75,6 +76,10 @@ 'assignment_type', 'extra_info', 'first_component_block_id'] ) +_Funix_Assignment = namedtuple( + 'Assignment', ['block_key', 'title', 'url', 'date', 'contains_gated_content', 'complete', 'past_due', 'assignment_type', 'extra_info', 'first_component_block_id', 'complete_date'] +) + def get_course(course_id, depth=0): """ @@ -931,14 +936,13 @@ def get_course_granded_lesson(course_key, user, include_access=False): graded = block_data.get_xblock_field(subsection_key, 'graded', False) if graded: effort_time = block_data.get_xblock_field(subsection_key, 'effort_time', -1) - print('--sdsd---') - print(effort_time) first_component_block_id = get_first_component_of_block(subsection_key, block_data) contains_gated_content = include_access and block_data.get_xblock_field( subsection_key, 'contains_gated_content', False) title = block_data.get_xblock_field(subsection_key, 'display_name', _('Assignment')) assignment_type = block_data.get_xblock_field(subsection_key, 'format', None) + complete_date = block_data.get_xblock_field(subsection_key, BlockCompletionTransformer.COMPLETE_TIME, None) url = None start = block_data.get_xblock_field(subsection_key, 'start') @@ -951,9 +955,9 @@ def get_course_granded_lesson(course_key, user, include_access=False): # past_due = not complete and due < now past_due = False - assignments.append(_Assignment( + assignments.append(_Funix_Assignment( subsection_key, title, url, due, contains_gated_content, - complete, past_due, assignment_type, None, first_component_block_id + complete, past_due, assignment_type, None, first_component_block_id, complete_date )) return assignments @@ -969,6 +973,7 @@ def funix_get_assginment_date_blocks(course, user, request, num_return=None, inc date_block.assignment_type = assignment.assignment_type date_block.past_due = assignment.past_due date_block.block_key = assignment.block_key + date_block.complete_date = assignment.complete_date # date_block.link = request.build_absolute_uri(assignment.url) if assignment.url else '' date_block.set_title(assignment.title, link=assignment.url) date_block._extra_info = assignment.extra_info # pylint: disable=protected-access diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index 7cce2d18114a..047b7823efb0 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -410,6 +410,7 @@ def __init__(self, *args, **kwargs): self.past_due = None self._extra_info = None self.block_key = None + self.complete_date = None @property def date(self): diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index e9520ac299f2..df996978d891 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -18,8 +18,6 @@ def get_schedule(self, user_name, course_id): last_complete = self._get_last_complete_assignment(assignment_blocks=assignment_blocks) - last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id) + last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id).date if last_complete is not None: - last_complete_date = FunixRelativeDateDAO.get_block_by_id(user_id=user.id, course_id=course_id, block_id=last_complete.subsection_key) - - print(last_complete_date) + last_complete_date = last_complete.complete_date From 660802a47a2ede17f4c8d25913c708112d9c185d Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 28 Dec 2021 10:40:07 +0000 Subject: [PATCH 009/519] [ADD] schedule date in funix relative date --- .../funix_relative_date.py | 28 +++++++++++++++---- .../features/funix_relative_date/models.py | 10 ++++++- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index df996978d891..0cea694f7c34 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -1,6 +1,8 @@ +from datetime import timedelta + from lms.djangoapps.courseware.courses import funix_get_assginment_date_blocks, get_course_with_access from common.djangoapps.student.models import get_user_by_username_or_email -from openedx.features.funix_relative_date.models import FunixRelativeDateDAO +from openedx.features.funix_relative_date.models import FunixRelativeDate, FunixRelativeDateDAO from opaque_keys.edx.keys import CourseKey class FunixRelativeDateLibary(): @@ -8,7 +10,6 @@ class FunixRelativeDateLibary(): def _get_last_complete_assignment(self, assignment_blocks): return next((asm for asm in assignment_blocks[::-1] if asm.complete), None) - @classmethod def get_schedule(self, user_name, course_id): user = get_user_by_username_or_email(user_name) @@ -16,8 +17,23 @@ def get_schedule(self, user_name, course_id): course = get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) assignment_blocks = funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) - last_complete = self._get_last_complete_assignment(assignment_blocks=assignment_blocks) - last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id).date - if last_complete is not None: - last_complete_date = last_complete.complete_date + + # Delete all old date + FunixRelativeDateDAO.delete_all_date(user_id=user.id, course_id=course_id) + + index = 0 + completed_assignments = [asm for asm in assignment_blocks if asm.complete] + uncompleted_assignments = [asm for asm in assignment_blocks if not asm.complete] + + completed_assignments.sort(key=lambda x: x.complete_date) + for asm in completed_assignments: + index += 1 + last_complete_date = asm.complete_date + FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=asm.block_key, type='block', index=index, date=last_complete_date).save() + + for asm in uncompleted_assignments: + index += 1 + last_complete_date += timedelta(days=1) + + FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=asm.block_key, type='block', index=index, date=last_complete_date).save() diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py index 688ae06417b3..9779f02c13aa 100644 --- a/openedx/features/funix_relative_date/models.py +++ b/openedx/features/funix_relative_date/models.py @@ -1,4 +1,5 @@ from django.db import models +from django.db.models import Q from django.utils.timezone import now class FunixRelativeDate(models.Model): @@ -17,7 +18,14 @@ class FunixRelativeDateDAO(): def get_block_by_id(self, user_id, course_id, block_id): return FunixRelativeDate.objects.get(user_id=user_id, course_id=course_id, block_id=block_id) + @classmethod + def delete_all_date(self, user_id, course_id): + return FunixRelativeDate.objects.filter( + ~Q(type='start'), + user_id=user_id, + course_id=course_id + ).delete() + @classmethod def get_enroll_by_id(self, user_id, course_id): - print('43-23', user_id, course_id) return FunixRelativeDate.objects.get(user_id=user_id, course_id=course_id, type="start") From 95f7e55b1132111384a1af525b268a6184518bd5 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 28 Dec 2021 15:57:02 +0000 Subject: [PATCH 010/519] [ADD] Add funix date API --- lms/djangoapps/course_home_api/urls.py | 10 +++ lms/djangoapps/courseware/date_summary.py | 51 ++++++++++++ .../funix_relative_date.py | 40 ++++++++- .../features/funix_relative_date/models.py | 7 ++ openedx/features/funix_relative_date/views.py | 83 +++++++++++++++++++ 5 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 openedx/features/funix_relative_date/views.py diff --git a/lms/djangoapps/course_home_api/urls.py b/lms/djangoapps/course_home_api/urls.py index b5ffc08481a7..c14dbc74ba10 100644 --- a/lms/djangoapps/course_home_api/urls.py +++ b/lms/djangoapps/course_home_api/urls.py @@ -7,6 +7,7 @@ from django.urls import re_path from lms.djangoapps.course_home_api.course_metadata.views import CourseHomeMetadataView +from openedx.features.funix_relative_date.views import FunixRelativeDatesTabView from lms.djangoapps.course_home_api.dates.views import DatesTabView from lms.djangoapps.course_home_api.outline.views import ( OutlineTabView, dismiss_welcome_message, save_course_goal, unsubscribe_from_course_goal_by_token, @@ -37,6 +38,15 @@ ), ] +# Funix Dates Tab URLs +urlpatterns += [ + re_path( + fr'dates-funix/{settings.COURSE_KEY_PATTERN}', + FunixRelativeDatesTabView.as_view(), + name='funix-dates-tab' + ), +] + # Outline Tab URLs urlpatterns += [ re_path( diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index 047b7823efb0..6c6fe242f93c 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -735,3 +735,54 @@ def verification_status(self): def must_retry(self): """Return True if the user must re-submit verification, False otherwise.""" return self.verification_status == 'must_reverify' + +class FunixCourseStartDate(DateSummary): + """ + Displays the start date of the course. + """ + css_class = 'start-date' + + @property + def date(self): + return self._init_date + + @property + def date_type(self): + return 'course-start-date' + + @property + def title(self): + return gettext_lazy('Enrollment Date') + + def register_alerts(self, request, course): + """ + Registers an alert if the course has not started yet. + """ + is_enrolled = CourseEnrollment.get_enrollment(request.user, course.id) + if not course.start or not is_enrolled: + return + days_until_start = (course.start - self.current_time).days + if course.start > self.current_time: + if days_until_start > 0: + CourseHomeMessages.register_info_message( + request, + Text(_( + "Don't forget to add a calendar reminder!" + )), + title=Text(_("Course starts in {time_remaining_string} on {course_start_date}.")).format( + time_remaining_string=self.time_remaining_string, + course_start_date=self.long_date_html, + ) + ) + else: + CourseHomeMessages.register_info_message( + request, + Text(_("Course starts in {time_remaining_string} at {course_start_time}.")).format( + time_remaining_string=self.time_remaining_string, + course_start_time=self.short_time_html, + ) + ) + + def __init__(self, course, user, course_id=None, date=None): + super().__init__(course, user, course_id=course_id) + self._init_date = date diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 0cea694f7c34..2b8014f74154 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -1,14 +1,48 @@ -from datetime import timedelta +from datetime import timedelta, datetime, time +import pytz +from django.urls import reverse from lms.djangoapps.courseware.courses import funix_get_assginment_date_blocks, get_course_with_access from common.djangoapps.student.models import get_user_by_username_or_email from openedx.features.funix_relative_date.models import FunixRelativeDate, FunixRelativeDateDAO from opaque_keys.edx.keys import CourseKey +from lms.djangoapps.courseware.date_summary import FunixCourseStartDate, TodaysDate class FunixRelativeDateLibary(): @classmethod - def _get_last_complete_assignment(self, assignment_blocks): - return next((asm for asm in assignment_blocks[::-1] if asm.complete), None) + def _date_to_datetime(self, date): + return pytz.utc.localize(datetime.combine(date, time(0, 0))) + + @classmethod + def get_course_date_blocks(self, course, user, request=None): + assignment_blocks = funix_get_assginment_date_blocks(course=course, user=user, request=request, include_past_dates=True) + date_blocks = FunixRelativeDateDAO.get_all_block_by_id(user_id=user.id, course_id=course.id) + date_blocks = list(date_blocks) + date_blocks.sort(key=lambda x: x.index) + + # Add start date + start_date = date_blocks.pop(0) + output = [ + FunixCourseStartDate(course=course, user=user, date=self._date_to_datetime(start_date.date)), + TodaysDate(course=course, user=user) + ] + + date_dict = { + str(asm.block_id): asm.date + for asm in date_blocks + } + for asm in assignment_blocks: + block_key = str(asm.block_key) + if block_key in date_dict: + asm.date = self._date_to_datetime(date_dict[block_key]) + + link = reverse('jump_to', args=[str(course.id), block_key]) + link = request.build_absolute_uri(link) if link else '' + asm.link = link + + output.append(asm) + + return sorted(output, key=lambda b: b.date) @classmethod def get_schedule(self, user_name, course_id): diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py index 9779f02c13aa..0c67f5f83657 100644 --- a/openedx/features/funix_relative_date/models.py +++ b/openedx/features/funix_relative_date/models.py @@ -26,6 +26,13 @@ def delete_all_date(self, user_id, course_id): course_id=course_id ).delete() + @classmethod + def get_all_block_by_id(self, user_id, course_id): + return FunixRelativeDate.objects.filter( + user_id=user_id, + course_id=course_id + ) + @classmethod def get_enroll_by_id(self, user_id, course_id): return FunixRelativeDate.objects.get(user_id=user_id, course_id=course_id, type="start") diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py new file mode 100644 index 000000000000..5c574ac90b13 --- /dev/null +++ b/openedx/features/funix_relative_date/views.py @@ -0,0 +1,83 @@ +""" +Dates Tab Views +""" + +from django.http.response import Http404 +from edx_django_utils import monitoring as monitoring_utils +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser +from opaque_keys.edx.keys import CourseKey +from rest_framework.generics import RetrieveAPIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from common.djangoapps.student.models import CourseEnrollment +from lms.djangoapps.course_goals.models import UserActivity +from lms.djangoapps.course_home_api.dates.serializers import DatesTabSerializer +from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active +from lms.djangoapps.courseware.access import has_access +from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs +from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access +from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary +from lms.djangoapps.courseware.date_summary import TodaysDate +from lms.djangoapps.courseware.masquerade import setup_masquerade +from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser +from openedx.features.content_type_gating.models import ContentTypeGatingConfig + + +class FunixRelativeDatesTabView(RetrieveAPIView): + authentication_classes = ( + JwtAuthentication, + BearerAuthenticationAllowInactiveUser, + SessionAuthenticationAllowInactiveUser, + ) + permission_classes = (IsAuthenticated,) + serializer_class = DatesTabSerializer + + def get(self, request, *args, **kwargs): + course_key_string = kwargs.get('course_key_string') + course_key = CourseKey.from_string(course_key_string) + + if course_home_legacy_is_active(course_key): + raise Http404 + + # Enable NR tracing for this view based on course + monitoring_utils.set_custom_attribute('course_id', course_key_string) + monitoring_utils.set_custom_attribute('user_id', request.user.id) + monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) + + course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) + is_staff = bool(has_access(request.user, 'staff', course_key)) + + _, request.user = setup_masquerade( + request, + course_key, + staff_access=is_staff, + reset_masquerade_data=True, + ) + + if not CourseEnrollment.is_enrolled(request.user, course_key) and not is_staff: + return Response('User not enrolled.', status=401) + + blocks = FunixRelativeDateLibary.get_course_date_blocks(course=course,user=request.user, request=request) + + learner_is_full_access = not ContentTypeGatingConfig.enabled_for_enrollment( + user=request.user, + course_key=course_key, + ) + + # User locale settings + user_timezone_locale = user_timezone_locale_prefs(request) + user_timezone = user_timezone_locale['user_timezone'] + + data = { + 'has_ended': course.has_ended(), + 'course_date_blocks': [block for block in blocks if not isinstance(block, TodaysDate)], + 'learner_is_full_access': learner_is_full_access, + 'user_timezone': user_timezone, + } + context = self.get_serializer_context() + context['learner_is_full_access'] = learner_is_full_access + serializer = self.get_serializer_class()(data, context=context) + + return Response(serializer.data) From 1d32b70126fca058164e5b8fbf5776a7a06becb6 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 28 Dec 2021 16:59:21 +0000 Subject: [PATCH 011/519] [CHANGE] Change funix relative date algorithm --- lms/djangoapps/courseware/courses.py | 5 ++-- lms/djangoapps/courseware/date_summary.py | 1 + .../funix_relative_date.py | 27 ++++++++++++++++--- 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index b0c4aa068c7b..688ffcedb634 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -77,7 +77,7 @@ ) _Funix_Assignment = namedtuple( - 'Assignment', ['block_key', 'title', 'url', 'date', 'contains_gated_content', 'complete', 'past_due', 'assignment_type', 'extra_info', 'first_component_block_id', 'complete_date'] + 'Assignment', ['block_key', 'title', 'url', 'date', 'contains_gated_content', 'complete', 'past_due', 'assignment_type', 'extra_info', 'first_component_block_id', 'complete_date', 'effort_time'] ) @@ -957,7 +957,7 @@ def get_course_granded_lesson(course_key, user, include_access=False): past_due = False assignments.append(_Funix_Assignment( subsection_key, title, url, due, contains_gated_content, - complete, past_due, assignment_type, None, first_component_block_id, complete_date + complete, past_due, assignment_type, None, first_component_block_id, complete_date, effort_time )) return assignments @@ -974,6 +974,7 @@ def funix_get_assginment_date_blocks(course, user, request, num_return=None, inc date_block.past_due = assignment.past_due date_block.block_key = assignment.block_key date_block.complete_date = assignment.complete_date + date_block.effort_time = assignment.effort_time // 60 # date_block.link = request.build_absolute_uri(assignment.url) if assignment.url else '' date_block.set_title(assignment.title, link=assignment.url) date_block._extra_info = assignment.extra_info # pylint: disable=protected-access diff --git a/lms/djangoapps/courseware/date_summary.py b/lms/djangoapps/courseware/date_summary.py index 6c6fe242f93c..ebf39db98d27 100644 --- a/lms/djangoapps/courseware/date_summary.py +++ b/lms/djangoapps/courseware/date_summary.py @@ -411,6 +411,7 @@ def __init__(self, *args, **kwargs): self._extra_info = None self.block_key = None self.complete_date = None + self.effort_time = None @property def date(self): diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 2b8014f74154..a9dc308180f2 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -1,5 +1,6 @@ from datetime import timedelta, datetime, time import pytz +import math from django.urls import reverse from lms.djangoapps.courseware.courses import funix_get_assginment_date_blocks, get_course_with_access @@ -9,6 +10,9 @@ from lms.djangoapps.courseware.date_summary import FunixCourseStartDate, TodaysDate class FunixRelativeDateLibary(): + TIME_PER_DAY = 2.5 * 60 + + @classmethod def _date_to_datetime(self, date): return pytz.utc.localize(datetime.combine(date, time(0, 0))) @@ -66,8 +70,25 @@ def get_schedule(self, user_name, course_id): last_complete_date = asm.complete_date FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=asm.block_key, type='block', index=index, date=last_complete_date).save() + left_time = self.TIME_PER_DAY + arr = [] for asm in uncompleted_assignments: - index += 1 - last_complete_date += timedelta(days=1) + effort_time = asm.effort_time + if effort_time <= left_time: + arr.append(asm) + left_time -= effort_time + else: + last_complete_date += timedelta(days=1) + for el in arr: + index += 1 + FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=el.block_key, type='block', index=index, date=last_complete_date).save() + left_time = self.TIME_PER_DAY + if effort_time > self.TIME_PER_DAY: + index += 1 - FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=asm.block_key, type='block', index=index, date=last_complete_date).save() + day_need = math.ceil(effort_time / self.TIME_PER_DAY) + last_complete_date += timedelta(days=day_need) + FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=asm.block_key, type='block', index=index, date=last_complete_date).save() + arr = [] + else: + arr = [asm] From a40d26ab76153cd6d907b3ad2859c7a3f1fe0808 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Wed, 29 Dec 2021 04:45:04 +0000 Subject: [PATCH 012/519] [ADD] Reschedule when user complete lesson --- lms/djangoapps/courseware/module_render.py | 2 ++ .../features/funix_relative_date/funix_relative_date.py | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 828419d55a2c..4185ee9c01f2 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -96,6 +96,7 @@ from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.util.sandboxing import can_execute_unsafe_code, get_python_lib_zip # lint-amnesty, pylint: disable=wrong-import-order +from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary log = logging.getLogger(__name__) @@ -564,6 +565,7 @@ def handle_completion_event(block, event): block_key=block.scope_ids.usage_id, completion=event['completion'], ) + FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) def handle_grade_event(block, event): """ diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index a9dc308180f2..1957fac2db81 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -3,7 +3,7 @@ import math from django.urls import reverse -from lms.djangoapps.courseware.courses import funix_get_assginment_date_blocks, get_course_with_access +import lms.djangoapps.courseware.courses as courseware_courses from common.djangoapps.student.models import get_user_by_username_or_email from openedx.features.funix_relative_date.models import FunixRelativeDate, FunixRelativeDateDAO from opaque_keys.edx.keys import CourseKey @@ -19,7 +19,7 @@ def _date_to_datetime(self, date): @classmethod def get_course_date_blocks(self, course, user, request=None): - assignment_blocks = funix_get_assginment_date_blocks(course=course, user=user, request=request, include_past_dates=True) + assignment_blocks = courseware_courses.funix_get_assginment_date_blocks(course=course, user=user, request=request, include_past_dates=True) date_blocks = FunixRelativeDateDAO.get_all_block_by_id(user_id=user.id, course_id=course.id) date_blocks = list(date_blocks) date_blocks.sort(key=lambda x: x.index) @@ -52,8 +52,8 @@ def get_course_date_blocks(self, course, user, request=None): def get_schedule(self, user_name, course_id): user = get_user_by_username_or_email(user_name) course_key = CourseKey.from_string(course_id) - course = get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) - assignment_blocks = funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) + course = courseware_courses.get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) + assignment_blocks = courseware_courses.funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id).date @@ -92,3 +92,4 @@ def get_schedule(self, user_name, course_id): arr = [] else: arr = [asm] + left_time -= effort_time From 9e21a31b86cf3ba598cd921988c70b84684f636c Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Wed, 29 Dec 2021 07:27:57 +0000 Subject: [PATCH 013/519] [Add] Reschedule when publish course --- common/djangoapps/student/models.py | 2 ++ .../funix_relative_date/funix_relative_date.py | 10 ++++++++-- openedx/features/funix_relative_date/handlers.py | 2 +- openedx/features/funix_relative_date/models.py | 7 +++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index f976f493d4b9..defe247ac9e4 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -2624,6 +2624,8 @@ def get_user_by_username_or_email(username_or_email): raise User.DoesNotExist return user +def get_user_by_id(user_id): + return User.objects.get(id=user_id) def get_user(email): user = User.objects.get(email=email) diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 1957fac2db81..4a1fd712f09c 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -4,7 +4,7 @@ from django.urls import reverse import lms.djangoapps.courseware.courses as courseware_courses -from common.djangoapps.student.models import get_user_by_username_or_email +from common.djangoapps.student.models import get_user_by_username_or_email, get_user_by_id from openedx.features.funix_relative_date.models import FunixRelativeDate, FunixRelativeDateDAO from opaque_keys.edx.keys import CourseKey from lms.djangoapps.courseware.date_summary import FunixCourseStartDate, TodaysDate @@ -12,7 +12,6 @@ class FunixRelativeDateLibary(): TIME_PER_DAY = 2.5 * 60 - @classmethod def _date_to_datetime(self, date): return pytz.utc.localize(datetime.combine(date, time(0, 0))) @@ -93,3 +92,10 @@ def get_schedule(self, user_name, course_id): else: arr = [asm] left_time -= effort_time + + @classmethod + def re_schedule_by_course(self, course_id): + enroll_list = FunixRelativeDateDAO.get_all_enroll_by_course(course_id=course_id) + for user_el in enroll_list: + user = get_user_by_id(user_el.user_id) + self.get_schedule(user_name=user.username, course_id=str(course_id)) diff --git a/openedx/features/funix_relative_date/handlers.py b/openedx/features/funix_relative_date/handlers.py index a0ab3f90163b..dafa932eadb7 100644 --- a/openedx/features/funix_relative_date/handlers.py +++ b/openedx/features/funix_relative_date/handlers.py @@ -20,4 +20,4 @@ def handle_user_enroll(sender, event=None, user=None, course_id=None,**kwargs): @receiver(SignalHandler.course_published) def listen_for_course_publish(sender, course_key, **kwargs): - FunixRelativeDateLibary.get_schedule(user_name='edx',course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') + FunixRelativeDateLibary.re_schedule_by_course(course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py index 0c67f5f83657..4d9a93e0952d 100644 --- a/openedx/features/funix_relative_date/models.py +++ b/openedx/features/funix_relative_date/models.py @@ -36,3 +36,10 @@ def get_all_block_by_id(self, user_id, course_id): @classmethod def get_enroll_by_id(self, user_id, course_id): return FunixRelativeDate.objects.get(user_id=user_id, course_id=course_id, type="start") + + @classmethod + def get_all_enroll_by_course(self, course_id): + return FunixRelativeDate.objects.filter( + course_id=course_id, + type='start' + ) From 2783dfedc26a81ee4d66ffa12a67f92ef4872d01 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Wed, 29 Dec 2021 16:54:22 +0000 Subject: [PATCH 014/519] [ADD] Reschedule when complete Lab --- lms/djangoapps/courseware/module_render.py | 1 + .../core/djangoapps/xblock/runtime/runtime.py | 2 ++ .../funix_relative_date.py | 18 +++++++++++++--- .../features/funix_relative_date/handlers.py | 21 ++++++++++++++++++- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 4185ee9c01f2..840e46be6f69 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -610,6 +610,7 @@ def handle_deprecated_progress_event(block, event): block_key=block.scope_ids.usage_id, completion=1.0, ) + FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) def rebind_noauth_module_to_user(module, real_user): """ diff --git a/openedx/core/djangoapps/xblock/runtime/runtime.py b/openedx/core/djangoapps/xblock/runtime/runtime.py index 0b290ab2ab67..e60d73f3e5f2 100644 --- a/openedx/core/djangoapps/xblock/runtime/runtime.py +++ b/openedx/core/djangoapps/xblock/runtime/runtime.py @@ -34,6 +34,7 @@ from common.djangoapps.static_replace import process_static_urls from xmodule.errortracker import make_error_tracker # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import ModuleI18nService # lint-amnesty, pylint: disable=wrong-import-order +from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary from .id_managers import OpaqueKeyReader from .shims import RuntimeShim, XBlockShim @@ -189,6 +190,7 @@ def handle_completion_event(self, block, event): block_key=block.scope_ids.usage_id, completion=event['completion'], ) + FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) def applicable_aside_types(self, block): """ Disable XBlock asides in this runtime """ diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 4a1fd712f09c..48d39f0d4f4e 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -44,15 +44,27 @@ def get_course_date_blocks(self, course, user, request=None): asm.link = link output.append(asm) + output = sorted(output, key=lambda b: b.date) - return sorted(output, key=lambda b: b.date) + check_complete = True + for el in output: + if el.css_class == 'assignment': + if not el.complete: + check_complete = False + else: + if not check_complete: + self.get_schedule(user_name=user.username, course_id=str(course.id), assignment_blocks=assignment_blocks) + return self.get_course_date_blocks(course=course, user=user, request=request) + return output @classmethod - def get_schedule(self, user_name, course_id): + def get_schedule(self, user_name, course_id, assignment_blocks=None): user = get_user_by_username_or_email(user_name) course_key = CourseKey.from_string(course_id) course = courseware_courses.get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) - assignment_blocks = courseware_courses.funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) + + if assignment_blocks is None: + assignment_blocks = courseware_courses.funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id).date diff --git a/openedx/features/funix_relative_date/handlers.py b/openedx/features/funix_relative_date/handlers.py index dafa932eadb7..0ee4ee61799d 100644 --- a/openedx/features/funix_relative_date/handlers.py +++ b/openedx/features/funix_relative_date/handlers.py @@ -2,12 +2,14 @@ FunixRelativeDate related signal handlers. """ from django.dispatch import receiver - +from lms.djangoapps.grades.api import signals as grades_signals from common.djangoapps.student.models import EnrollStatusChange from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary from openedx.features.funix_relative_date.models import FunixRelativeDate from xmodule.modulestore.django import SignalHandler # lint-amnesty, pylint: disable=wrong-import-order +from common.djangoapps.student.models import get_user_by_id +import asyncio @receiver(ENROLL_STATUS_CHANGE) @@ -21,3 +23,20 @@ def handle_user_enroll(sender, event=None, user=None, course_id=None,**kwargs): @receiver(SignalHandler.course_published) def listen_for_course_publish(sender, course_key, **kwargs): FunixRelativeDateLibary.re_schedule_by_course(course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') + +# @receiver(grades_signals.PROBLEM_WEIGHTED_SCORE_CHANGED) +# def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument +# user_id = kwargs.get('user_id', None) +# course_id = kwargs.get('course_id', None) + +# user = get_user_by_id(user_id) +# asyncio.run(async_update_schedule(username=user.username, course_id=str(course_id))) +# print('--243-23554-342') + + + +# async def async_update_schedule(username, course_id): +# print('lkasjdfasdasd', username, course_id) +# await asyncio.sleep(5) +# print('--done--') +# FunixRelativeDateLibary.get_schedule(user_name=username, course_id=course_id) From 4517fc71ebeb90f20ef17d4f22729ffaf8b424dc Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Fri, 31 Dec 2021 03:00:15 +0000 Subject: [PATCH 015/519] [ADD] Open search --- cms/envs/common.py | 2 +- cms/envs/devstack.py | 15 ++++++++++++--- lms/envs/common.py | 4 ++-- lms/envs/devstack.py | 12 ++++++++++-- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 28d57962d879..46343ec770f1 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -281,7 +281,7 @@ 'LICENSING': False, # Enable the courseware search functionality - 'ENABLE_COURSEWARE_INDEX': False, + 'ENABLE_COURSEWARE_INDEX': True, # Enable content libraries (modulestore) search functionality 'ENABLE_LIBRARY_INDEX': False, diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 469bc20454fe..0971c2486e48 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -136,11 +136,20 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing XBLOCK_SETTINGS.update({'VideoBlock': {'licensing_enabled': True}}) ################################ SEARCH INDEX ################################ -FEATURES['ENABLE_COURSEWARE_INDEX'] = False -FEATURES['ENABLE_LIBRARY_INDEX'] = False -FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = False +FEATURES['ENABLE_COURSEWARE_INDEX'] = True +FEATURES['ENABLE_LIBRARY_INDEX'] = True +FEATURES['ENABLE_CONTENT_LIBRARY_INDEX'] = True SEARCH_ENGINE = "search.elastic.ElasticSearchEngine" +ELASTIC_SEARCH_CONFIG = [ + { + 'use_ssl': False, + 'host': '13.214.22.79', + 'port': 9201 + } +] + + ################################ COURSE DISCUSSIONS ########################### FEATURES['ENABLE_DISCUSSION_SERVICE'] = True diff --git a/lms/envs/common.py b/lms/envs/common.py index 7f3539254897..a16446dba71e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -570,7 +570,7 @@ # .. toggle_warnings: In order to get this working, your courses data should be indexed in Elasticsearch. You will # see the search widget on the courseware page only if the DISABLE_COURSE_OUTLINE_PAGE_FLAG is set. # .. toggle_tickets: https://github.com/edx/edx-platform/pull/6506 - 'ENABLE_COURSEWARE_SEARCH': False, + 'ENABLE_COURSEWARE_SEARCH': True, # .. toggle_name: FEATURES['ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF'] # .. toggle_implementation: DjangoSetting @@ -595,7 +595,7 @@ # .. toggle_creation_date: 2015-01-29 # .. toggle_warnings: In order to get this working, your courses data should be indexed in Elasticsearch. # .. toggle_tickets: https://github.com/edx/edx-platform/pull/6506 - 'ENABLE_DASHBOARD_SEARCH': False, + 'ENABLE_DASHBOARD_SEARCH': True, # log all information from cybersource callbacks 'LOG_POSTPAY_CALLBACKS': True, diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index fe422a1cefab..3084cb92dc03 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -163,13 +163,21 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing ########################## Courseware Search ####################### -FEATURES['ENABLE_COURSEWARE_SEARCH'] = False +FEATURES['ENABLE_COURSEWARE_SEARCH'] = True FEATURES['ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF'] = True SEARCH_ENGINE = 'search.elastic.ElasticSearchEngine' +ELASTIC_SEARCH_CONFIG = [ + { + 'use_ssl': False, + 'host': '13.214.22.79', + 'port': 9201 + } +] + ########################## Dashboard Search ####################### -FEATURES['ENABLE_DASHBOARD_SEARCH'] = False +FEATURES['ENABLE_DASHBOARD_SEARCH'] = True ########################## Certificates Web/HTML View ####################### From 2456372cd882929455d7463676d0b03341c0ec22 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Fri, 31 Dec 2021 04:47:14 +0000 Subject: [PATCH 016/519] [FIX] Fix bug when publish course --- openedx/features/funix_relative_date/handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/features/funix_relative_date/handlers.py b/openedx/features/funix_relative_date/handlers.py index 0ee4ee61799d..a47552c2e71e 100644 --- a/openedx/features/funix_relative_date/handlers.py +++ b/openedx/features/funix_relative_date/handlers.py @@ -19,11 +19,11 @@ def handle_user_enroll(sender, event=None, user=None, course_id=None,**kwargs): enrollment = FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=None, type='start', index=0) enrollment.save() + FunixRelativeDateLibary.get_schedule(user_name=user.username, course_id=str(course_id)) @receiver(SignalHandler.course_published) def listen_for_course_publish(sender, course_key, **kwargs): - FunixRelativeDateLibary.re_schedule_by_course(course_id='course-v1:FUNiX+DEP302x_01-A_VN+2021_T7') - + FunixRelativeDateLibary.re_schedule_by_course(course_id=str(course_key)) # @receiver(grades_signals.PROBLEM_WEIGHTED_SCORE_CHANGED) # def score_changed_handler(sender, **kwargs): # pylint: disable=unused-argument # user_id = kwargs.get('user_id', None) From 6291be6b606782af5b03239cc308139da7e8c656 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Wed, 5 Jan 2022 15:53:13 +0000 Subject: [PATCH 017/519] [FIX] Fix not writing track log --- cms/envs/devstack_docker.py | 9 +++++++++ lms/envs/devstack_docker.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/cms/envs/devstack_docker.py b/cms/envs/devstack_docker.py index 2eece814deae..0b9a768552c9 100644 --- a/cms/envs/devstack_docker.py +++ b/cms/envs/devstack_docker.py @@ -1,3 +1,12 @@ """ Overrides for Docker-based devstack. """ from .devstack import * # pylint: disable=wildcard-import, unused-wildcard-import + +LOGGING['handlers']['tracking'] = { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': '/edx/var/log/tracking/tracking.log', + 'formatter': 'raw', +} + +LOGGING['loggers']['tracking']['handlers'] = ['tracking'] diff --git a/lms/envs/devstack_docker.py b/lms/envs/devstack_docker.py index 7ac05251137b..ffce4f24e268 100644 --- a/lms/envs/devstack_docker.py +++ b/lms/envs/devstack_docker.py @@ -7,3 +7,12 @@ """ from .devstack import * # pylint: disable=wildcard-import, unused-wildcard-import + +LOGGING['handlers']['tracking'] = { + 'level': 'DEBUG', + 'class': 'logging.FileHandler', + 'filename': '/edx/var/log/tracking/tracking.log', + 'formatter': 'raw', +} + +LOGGING['loggers']['tracking']['handlers'] = ['tracking'] From 2c39d7566c9e2187bb94ca6ebf10d1ea4152db5d Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 10 Jan 2022 09:37:47 +0000 Subject: [PATCH 018/519] [FIX] Fix some bug --- lms/djangoapps/courseware/courses.py | 4 ++-- openedx/features/funix_relative_date/funix_relative_date.py | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index 688ffcedb634..a152adfb16de 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -934,8 +934,8 @@ def get_course_granded_lesson(course_key, user, include_access=False): for subsection_key in block_data.get_children(section_key): due = block_data.get_xblock_field(subsection_key, 'due', None) graded = block_data.get_xblock_field(subsection_key, 'graded', False) - if graded: - effort_time = block_data.get_xblock_field(subsection_key, 'effort_time', -1) + effort_time = block_data.get_xblock_field(subsection_key, 'effort_time', -1) + if graded and effort_time != -1: first_component_block_id = get_first_component_of_block(subsection_key, block_data) contains_gated_content = include_access and block_data.get_xblock_field( subsection_key, 'contains_gated_content', False) diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 48d39f0d4f4e..eb13a9efc82d 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -1,4 +1,4 @@ -from datetime import timedelta, datetime, time +from datetime import timedelta, datetime, time, date import pytz import math from django.urls import reverse @@ -67,6 +67,8 @@ def get_schedule(self, user_name, course_id, assignment_blocks=None): assignment_blocks = courseware_courses.funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id).date + if last_complete_date is None: + last_complete_date = date.today() # Delete all old date FunixRelativeDateDAO.delete_all_date(user_id=user.id, course_id=course_id) From d85b5699f13def3d85db40372cd9dc652aa9a192 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 10 Jan 2022 14:43:12 +0000 Subject: [PATCH 019/519] [FIX] Fix bug wrong complete block --- lms/djangoapps/courseware/courses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/courses.py b/lms/djangoapps/courseware/courses.py index a152adfb16de..fefe8cd3d7e9 100644 --- a/lms/djangoapps/courseware/courses.py +++ b/lms/djangoapps/courseware/courses.py @@ -949,7 +949,8 @@ def get_course_granded_lesson(course_key, user, include_access=False): assignment_released = not start or start < now if assignment_released: # url = reverse('jump_to', args=[course_key, subsection_key]) - complete = is_block_structure_complete_for_assignments(block_data, subsection_key) + complete = block_data.get_xblock_field(subsection_key, 'complete', False) + # complete = is_block_structure_complete_for_assignments(block_data, subsection_key) else: complete = False From 7275a940a024117c1a6c7dac2d744f9b2380ee86 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 11 Jan 2022 04:32:33 +0000 Subject: [PATCH 020/519] [FIX] Fix can't submit quiz if past due --- common/lib/xmodule/xmodule/capa_module.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py index f2d5575c16bb..1d30145a3a46 100644 --- a/common/lib/xmodule/xmodule/capa_module.py +++ b/common/lib/xmodule/xmodule/capa_module.py @@ -974,6 +974,11 @@ def should_enable_submit_button(self): # then we disable the "submit" button # Also, disable the "submit" button if we're waiting # for the user to reset a randomized problem + + # if Past Due => Can submit + if self.is_past_due(): + return True + if self.closed() or submitted_without_reset: return False else: From a13d6d25de0ad27949e151f7e71850354a542b7f Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 18 Jan 2022 04:37:57 +0000 Subject: [PATCH 021/519] [ADD] Add dd set_goal function --- cms/envs/common.py | 1 + lms/djangoapps/course_home_api/urls.py | 10 ++++++ lms/envs/common.py | 1 + openedx/features/funix_goal/apps.py | 6 ++++ .../funix_goal/migrations/0001_initial.py | 30 ++++++++++++++++ .../funix_goal/migrations/__init__.py | 0 openedx/features/funix_goal/models.py | 34 +++++++++++++++++++ openedx/features/funix_goal/views.py | 20 +++++++++++ 8 files changed, 102 insertions(+) create mode 100644 openedx/features/funix_goal/apps.py create mode 100644 openedx/features/funix_goal/migrations/0001_initial.py create mode 100644 openedx/features/funix_goal/migrations/__init__.py create mode 100644 openedx/features/funix_goal/models.py create mode 100644 openedx/features/funix_goal/views.py diff --git a/cms/envs/common.py b/cms/envs/common.py index 46343ec770f1..83f217c9349f 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1662,6 +1662,7 @@ 'openedx.features.discounts', 'openedx.features.effort_estimation', 'openedx.features.funix_relative_date', + 'openedx.features.funix_goal', 'lms.djangoapps.experiments', 'openedx.core.djangoapps.external_user_ids', diff --git a/lms/djangoapps/course_home_api/urls.py b/lms/djangoapps/course_home_api/urls.py index c14dbc74ba10..19b70fe7beaf 100644 --- a/lms/djangoapps/course_home_api/urls.py +++ b/lms/djangoapps/course_home_api/urls.py @@ -13,6 +13,7 @@ OutlineTabView, dismiss_welcome_message, save_course_goal, unsubscribe_from_course_goal_by_token, ) from lms.djangoapps.course_home_api.progress.views import ProgressTabView +from openedx.features.funix_goal.views import ( set_goal ) # This API is a BFF ("backend for frontend") designed for the learning MFE. It's not versioned because there is no # guarantee of stability over time. It may change from one open edx release to another. Don't write any scripts @@ -38,6 +39,15 @@ ), ] +# Funix Goal URLs +urlpatterns += [ + re_path( + r'set_goal', + set_goal, + name='set-goal' + ), +] + # Funix Dates Tab URLs urlpatterns += [ re_path( diff --git a/lms/envs/common.py b/lms/envs/common.py index 8848f5d29b61..2b77ea684f86 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3144,6 +3144,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring 'openedx.features.discounts', 'openedx.features.effort_estimation', 'openedx.features.funix_relative_date', + 'openedx.features.funix_goal', 'openedx.features.name_affirmation_api.apps.NameAffirmationApiConfig', 'lms.djangoapps.experiments', diff --git a/openedx/features/funix_goal/apps.py b/openedx/features/funix_goal/apps.py new file mode 100644 index 000000000000..a32c0debbc60 --- /dev/null +++ b/openedx/features/funix_goal/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FunixGoalConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'openedx.features.funix_goal' diff --git a/openedx/features/funix_goal/migrations/0001_initial.py b/openedx/features/funix_goal/migrations/0001_initial.py new file mode 100644 index 000000000000..ca67d87bb176 --- /dev/null +++ b/openedx/features/funix_goal/migrations/0001_initial.py @@ -0,0 +1,30 @@ +# Generated by Django 3.2.11 on 2022-01-18 04:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='LearnGoal', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user_id', models.CharField(max_length=255)), + ('course_id', models.CharField(max_length=255)), + ('hours_per_day', models.FloatField(default=2.5)), + ('weekday_0', models.BooleanField(default=True)), + ('weekday_1', models.BooleanField(default=True)), + ('weekday_2', models.BooleanField(default=True)), + ('weekday_3', models.BooleanField(default=True)), + ('weekday_4', models.BooleanField(default=True)), + ('weekday_5', models.BooleanField(default=False)), + ('weekday_6', models.BooleanField(default=False)), + ], + ), + ] diff --git a/openedx/features/funix_goal/migrations/__init__.py b/openedx/features/funix_goal/migrations/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/features/funix_goal/models.py b/openedx/features/funix_goal/models.py new file mode 100644 index 000000000000..d902aa7ab966 --- /dev/null +++ b/openedx/features/funix_goal/models.py @@ -0,0 +1,34 @@ +from django.db import models +from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary + + +class LearnGoal(models.Model): + user_id = models.CharField(max_length=255) + course_id = models.CharField(max_length=255) + hours_per_day = models.FloatField(default=2.5) + weekday_0 = models.BooleanField(default=True) + weekday_1 = models.BooleanField(default=True) + weekday_2 = models.BooleanField(default=True) + weekday_3 = models.BooleanField(default=True) + weekday_4 = models.BooleanField(default=True) + weekday_5 = models.BooleanField(default=False) + weekday_6 = models.BooleanField(default=False) + + @classmethod + def set_goal(self, course_id, user, hours_per_day, week_days): + user_id = str(user.id) + self.objects.filter(course_id=course_id, user_id=user_id).delete() + + goal_obj = LearnGoal(course_id=course_id, hours_per_day=hours_per_day, user_id=user_id) + + goal_obj.weekday_0 = week_days[0] + goal_obj.weekday_1 = week_days[1] + goal_obj.weekday_2 = week_days[2] + goal_obj.weekday_3 = week_days[3] + goal_obj.weekday_4 = week_days[4] + goal_obj.weekday_5 = week_days[5] + goal_obj.weekday_6 = week_days[6] + + goal_obj.save() + + return FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) diff --git a/openedx/features/funix_goal/views.py b/openedx/features/funix_goal/views.py new file mode 100644 index 000000000000..975d0d7453a6 --- /dev/null +++ b/openedx/features/funix_goal/views.py @@ -0,0 +1,20 @@ +from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication +from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser +from rest_framework.generics import RetrieveAPIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.decorators import api_view, authentication_classes, permission_classes # lint-amnesty, pylint: disable=wrong-import-order +from openedx.features.funix_goal.models import LearnGoal + + +@api_view(['POST']) +@authentication_classes((JwtAuthentication,)) +@permission_classes((IsAuthenticated,)) +def set_goal(request): + course_id = request.data.get('course_id') + hours_per_day = float(request.data.get('hours_per_day')) + week_days = list(request.data.get('week_days')) + + LearnGoal.set_goal(course_id=course_id, user=request.user, hours_per_day=hours_per_day, week_days=week_days) + + return Response(status=202) From 2de90fdabee0cfb77ef594b5027a57e920fae705 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 18 Jan 2022 05:46:52 +0000 Subject: [PATCH 022/519] [CHANGE] Change get_schedule method logic --- openedx/features/funix_goal/models.py | 11 +++++++-- .../funix_relative_date.py | 24 ++++++++++++++----- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/openedx/features/funix_goal/models.py b/openedx/features/funix_goal/models.py index d902aa7ab966..80fef9d5c45c 100644 --- a/openedx/features/funix_goal/models.py +++ b/openedx/features/funix_goal/models.py @@ -1,5 +1,5 @@ from django.db import models -from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary +import openedx.features.funix_relative_date.funix_relative_date as FunixRelativeDateModule class LearnGoal(models.Model): @@ -31,4 +31,11 @@ def set_goal(self, course_id, user, hours_per_day, week_days): goal_obj.save() - return FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) + return FunixRelativeDateModule.FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) + + @classmethod + def get_goal(self, course_id, user_id): + try: + return self.objects.filter(course_id=course_id, user_id=user_id).get() + except: + return LearnGoal(course_id=course_id, user_id=user_id) diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index eb13a9efc82d..322c1381ca80 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -8,6 +8,7 @@ from openedx.features.funix_relative_date.models import FunixRelativeDate, FunixRelativeDateDAO from opaque_keys.edx.keys import CourseKey from lms.djangoapps.courseware.date_summary import FunixCourseStartDate, TodaysDate +from openedx.features.funix_goal.models import LearnGoal class FunixRelativeDateLibary(): TIME_PER_DAY = 2.5 * 60 @@ -59,6 +60,15 @@ def get_course_date_blocks(self, course, user, request=None): @classmethod def get_schedule(self, user_name, course_id, assignment_blocks=None): + def get_time(last_complete_date, goal, day=1): + last_complete_date += timedelta(days=day) + weekday = last_complete_date.weekday() + + if not getattr(goal, f'weekday_{weekday}'): + return get_time(last_complete_date, goal) + + return last_complete_date + user = get_user_by_username_or_email(user_name) course_key = CourseKey.from_string(course_id) course = courseware_courses.get_course_with_access(user, 'load', course_key=course_key, check_if_enrolled=False) @@ -73,6 +83,8 @@ def get_schedule(self, user_name, course_id, assignment_blocks=None): # Delete all old date FunixRelativeDateDAO.delete_all_date(user_id=user.id, course_id=course_id) + # Get goal + goal = LearnGoal.get_goal(course_id=course_id, user_id=str(user.id)) index = 0 completed_assignments = [asm for asm in assignment_blocks if asm.complete] uncompleted_assignments = [asm for asm in assignment_blocks if not asm.complete] @@ -83,7 +95,7 @@ def get_schedule(self, user_name, course_id, assignment_blocks=None): last_complete_date = asm.complete_date FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=asm.block_key, type='block', index=index, date=last_complete_date).save() - left_time = self.TIME_PER_DAY + left_time = float(goal.hours_per_day) * 60 arr = [] for asm in uncompleted_assignments: effort_time = asm.effort_time @@ -91,16 +103,16 @@ def get_schedule(self, user_name, course_id, assignment_blocks=None): arr.append(asm) left_time -= effort_time else: - last_complete_date += timedelta(days=1) + last_complete_date = get_time(last_complete_date, goal) for el in arr: index += 1 FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=el.block_key, type='block', index=index, date=last_complete_date).save() - left_time = self.TIME_PER_DAY - if effort_time > self.TIME_PER_DAY: + left_time = float(goal.hours_per_day) * 60 + if effort_time > left_time: index += 1 - day_need = math.ceil(effort_time / self.TIME_PER_DAY) - last_complete_date += timedelta(days=day_need) + day_need = math.ceil(effort_time / left_time) + last_complete_date = get_time(last_complete_date, goal, day=day_need) FunixRelativeDate(user_id=user.id, course_id=str(course_id), block_id=asm.block_key, type='block', index=index, date=last_complete_date).save() arr = [] else: From 3abe30ba49fb8d2c42ebdebf5efb202a1fe6f1fc Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Tue, 18 Jan 2022 10:04:02 +0000 Subject: [PATCH 023/519] [ADD] Get Goal data via API --- openedx/features/funix_relative_date/serializers.py | 11 +++++++++++ openedx/features/funix_relative_date/views.py | 10 ++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 openedx/features/funix_relative_date/serializers.py diff --git a/openedx/features/funix_relative_date/serializers.py b/openedx/features/funix_relative_date/serializers.py new file mode 100644 index 000000000000..eb9dfc6e5f22 --- /dev/null +++ b/openedx/features/funix_relative_date/serializers.py @@ -0,0 +1,11 @@ +from rest_framework import serializers + +from lms.djangoapps.course_home_api.dates.serializers import DatesTabSerializer + + +class FUNiXDatesTabSerializer(DatesTabSerializer): + """ + Serializer for the FUNiX Dates Tab + """ + goal_hours_per_day = serializers.FloatField() + goal_weekdays = serializers.ListField(child=serializers.BooleanField()) diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py index 5c574ac90b13..55bffcca65bf 100644 --- a/openedx/features/funix_relative_date/views.py +++ b/openedx/features/funix_relative_date/views.py @@ -13,7 +13,7 @@ from common.djangoapps.student.models import CourseEnrollment from lms.djangoapps.course_goals.models import UserActivity -from lms.djangoapps.course_home_api.dates.serializers import DatesTabSerializer +from openedx.features.funix_relative_date.serializers import FUNiXDatesTabSerializer from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs @@ -23,6 +23,7 @@ from lms.djangoapps.courseware.masquerade import setup_masquerade from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.features.content_type_gating.models import ContentTypeGatingConfig +from openedx.features.funix_goal.models import LearnGoal class FunixRelativeDatesTabView(RetrieveAPIView): @@ -32,7 +33,7 @@ class FunixRelativeDatesTabView(RetrieveAPIView): SessionAuthenticationAllowInactiveUser, ) permission_classes = (IsAuthenticated,) - serializer_class = DatesTabSerializer + serializer_class = FUNiXDatesTabSerializer def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') @@ -70,11 +71,16 @@ def get(self, request, *args, **kwargs): user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] + # Get goal + goal = LearnGoal.get_goal(course_id=course_key_string, user_id=str(request.user.id)) + data = { 'has_ended': course.has_ended(), 'course_date_blocks': [block for block in blocks if not isinstance(block, TodaysDate)], 'learner_is_full_access': learner_is_full_access, 'user_timezone': user_timezone, + 'goal_hours_per_day': goal.hours_per_day, + 'goal_weekdays': [getattr(goal, f'weekday_{i}') for i in range(7)] } context = self.get_serializer_context() context['learner_is_full_access'] = learner_is_full_access From a1b120dd31367520c04879ff20c08fc9fbe74904 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Sun, 23 Jan 2022 05:56:20 +0000 Subject: [PATCH 024/519] [ADD] Turn on Language Selector --- cms/envs/common.py | 2 +- lms/envs/common.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cms/envs/common.py b/cms/envs/common.py index 19985e753d00..3f366916edba 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -322,7 +322,7 @@ 'ENABLE_SPECIAL_EXAMS': False, # Show the language selector in the header - 'SHOW_HEADER_LANGUAGE_SELECTOR': False, + 'SHOW_HEADER_LANGUAGE_SELECTOR': True, # At edX it's safe to assume that English transcripts are always available # This is not the case for all installations. diff --git a/lms/envs/common.py b/lms/envs/common.py index 2b77ea684f86..4a57c944dd38 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -717,7 +717,7 @@ # .. toggle_warnings: You should set the languages in the DarkLangConfig table to get this working. If you have # not set any languages in the DarkLangConfig table then the language selector will not be visible in the header. # .. toggle_tickets: https://github.com/edx/edx-platform/pull/15133 - 'SHOW_HEADER_LANGUAGE_SELECTOR': False, + 'SHOW_HEADER_LANGUAGE_SELECTOR': True, # At edX it's safe to assume that English transcripts are always available # This is not the case for all installations. From 54224bef272bf78bb85eb549bcdf91fe26e0af03 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 2 May 2022 12:25:00 +0000 Subject: [PATCH 025/519] [FIX] Disable DISABLE_ESTIMATION code --- openedx/features/effort_estimation/block_transformers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openedx/features/effort_estimation/block_transformers.py b/openedx/features/effort_estimation/block_transformers.py index 15bdc54ed9ff..c7b710dc1960 100644 --- a/openedx/features/effort_estimation/block_transformers.py +++ b/openedx/features/effort_estimation/block_transformers.py @@ -88,7 +88,10 @@ def collect(cls, block_structure): # Some bit of required data is missing. Likely some duration info is missing from the video pipeline. # Rather than attempt to work around it, just set a note for ourselves to not show durations for this # course at all. Better no estimate than a misleading estimate. - block_structure.set_transformer_data(cls, cls.DISABLE_ESTIMATION, True) + + # Funix ==> Disable this code + # block_structure.set_transformer_data(cls, cls.DISABLE_ESTIMATION, True) + pass @classmethod def _collect_html_effort(cls, block_structure, block_key, xblock, _cache): From 8cc6c3f0e12620c8e0473c8ee979f334f882d23f Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 2 May 2022 14:07:15 +0000 Subject: [PATCH 026/519] [CHANGE] Change extract time method --- .../estimate_regex/estimate_time_by_regex.py | 59 +++++++++++++++++-- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py b/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py index 22d68b68f941..063a21fe79cb 100644 --- a/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py +++ b/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py @@ -5,8 +5,9 @@ import re import math -TIME_HOUR_REGEX = r"([0-9]+\.*[0-9]*) giờ" -TIME_MINUTE_REGEX = r"([0-9]+) phút" +TIME_HOUR_REGEX = r"([0-9]+\.*[0-9]*) gio" +TIME_MINUTE_REGEX = r"([0-9]+) phut" +NUMBER_REGEX = r"([0-9]+\.*[0-9]*)" def _get_hour_time(line): """ @@ -32,6 +33,29 @@ def _get_minute_time(line): return minute +def no_accent_vietnamese(s): + s = re.sub(r'[àáạảãâầấậẩẫăằắặẳẵ]', 'a', s) + s = re.sub(r'[ÀÁẠẢÃĂẰẮẶẲẴÂẦẤẬẨẪ]', 'A', s) + s = re.sub(r'[èéẹẻẽêềếệểễ]', 'e', s) + s = re.sub(r'[ÈÉẸẺẼÊỀẾỆỂỄ]', 'E', s) + s = re.sub(r'[òóọỏõôồốộổỗơờớợởỡ]', 'o', s) + s = re.sub(r'[ÒÓỌỎÕÔỒỐỘỔỖƠỜỚỢỞỠ]', 'O', s) + s = re.sub(r'[ìíịỉĩ]', 'i', s) + s = re.sub(r'[ÌÍỊỈĨ]', 'I', s) + s = re.sub(r'[ùúụủũưừứựửữ]', 'u', s) + s = re.sub(r'[ƯỪỨỰỬỮÙÚỤỦŨ]', 'U', s) + s = re.sub(r'[ỳýỵỷỹ]', 'y', s) + s = re.sub(r'[ỲÝỴỶỸ]', 'Y', s) + s = re.sub(r'[Đ]', 'D', s) + s = re.sub(r'[đ]', 'd', s) + + marks_list = [u'\u0300', u'\u0301', u'\u0302', u'\u0303', u'\u0306',u'\u0309', u'\u0323'] + + for mark in marks_list: + s = s.replace(mark, '') + + return s + def estimate_time_by_regex(text): """Estimate time effort using regex to extract time data from text @@ -48,11 +72,34 @@ def estimate_time_by_regex(text): time_by_regex = None last_line = lines[-1].lower() - hour = _get_hour_time(last_line) - minute = _get_minute_time(last_line) + last_line = no_accent_vietnamese(last_line) + + if 'gio' in last_line or 'phut' in last_line: + return None + # Old method: extract minute and hour from last line + # hour = _get_hour_time(last_line) + # minute = _get_minute_time(last_line) + + # if hour != 0 or minute != 0: + # time_by_regex = math.ceil(hour * 60) + minute + # time_by_regex = math.ceil(time_by_regex / 5) * 5 + + # New method: extract number from last line + regex_result = re.findall(NUMBER_REGEX, last_line) + + # Case 1: only one result => it is hours + if len(regex_result) == 1: + time_by_regex = float(regex_result[-1]) * 60 + # Case 2: two result => it is hours and minutes + elif len(regex_result) >= 2: + time_by_regex = float(regex_result[-2]) * 60 + time_by_regex += float(regex_result[-1]) + # Case 3: no result => return None + else: + time_by_regex = None - if hour != 0 or minute != 0: - time_by_regex = math.ceil(hour * 60) + minute + # if time is not None than round it to 5 min + if time_by_regex is not None: time_by_regex = math.ceil(time_by_regex / 5) * 5 return time_by_regex From 88d57d1fc71f01f07c65bcd58109d655ad9347e8 Mon Sep 17 00:00:00 2001 From: "D.A.N_3002" Date: Mon, 2 May 2022 14:53:19 +0000 Subject: [PATCH 027/519] [FIX] Fix regex bug --- .../estimate_regex/estimate_time_by_regex.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py b/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py index 063a21fe79cb..df29f3182f5c 100644 --- a/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py +++ b/openedx/features/effort_estimation/estimate_regex/estimate_time_by_regex.py @@ -74,8 +74,6 @@ def estimate_time_by_regex(text): last_line = no_accent_vietnamese(last_line) - if 'gio' in last_line or 'phut' in last_line: - return None # Old method: extract minute and hour from last line # hour = _get_hour_time(last_line) # minute = _get_minute_time(last_line) @@ -100,6 +98,9 @@ def estimate_time_by_regex(text): # if time is not None than round it to 5 min if time_by_regex is not None: - time_by_regex = math.ceil(time_by_regex / 5) * 5 + if 'gio' in last_line or 'phut' in last_line: + time_by_regex = math.ceil(time_by_regex / 5) * 5 + else: + return None return time_by_regex From 73d7560e4ec3bf5267095938e387353c46f0e3dc Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric Date: Tue, 11 Oct 2022 19:54:26 +0200 Subject: [PATCH 028/519] build: kickoff the Olive release We follow the instructions from: https://openedx.atlassian.net/wiki/spaces/COMM/pages/19662426/Process+to+Create+an+Open+edX+Release#Make-release-specific-changes-on-the-release-branch --- .github/pull_request_template.md | 17 ----------------- .tx/config | 11 +++++++++++ openedx/core/release.py | 2 +- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 0c61d1fb74fa..ed907c803152 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,20 +1,3 @@ - - ## Description Describe what this pull request changes, and why. Include implications for people using this change. diff --git a/.tx/config b/.tx/config index 5ab82585d37b..f9b8d235598e 100644 --- a/.tx/config +++ b/.tx/config @@ -67,3 +67,14 @@ source_file = conf/locale/en/LC_MESSAGES/wiki.po source_lang = en type = PO +[o:open-edx:p:open-edx-releases:r:release-olive] +file_filter = conf/locale//LC_MESSAGES/django.po +source_file = conf/locale/en/LC_MESSAGES/django.po +source_lang = en +type = PO + +[o:open-edx:p:open-edx-releases:r:release-olive-js] +file_filter = conf/locale//LC_MESSAGES/djangojs.po +source_file = conf/locale/en/LC_MESSAGES/djangojs.po +source_lang = en +type = PO diff --git a/openedx/core/release.py b/openedx/core/release.py index ce30df8cc543..295b8b1f014a 100644 --- a/openedx/core/release.py +++ b/openedx/core/release.py @@ -8,7 +8,7 @@ # The release line: an Open edX release name ("ficus"), or "master". # This should always be "master" on the master branch, and will be changed # manually when we start release-line branches, like open-release/ficus.master. -RELEASE_LINE = "master" +RELEASE_LINE = "olive" def doc_version(): From b2305b4ca3a2964c593a3af107b87180e6997333 Mon Sep 17 00:00:00 2001 From: Soban Javed Date: Thu, 17 Nov 2022 01:04:38 +0500 Subject: [PATCH 029/519] build: run tests for open-release branches on GH hosted runners --- .github/workflows/unit-tests-gh-hosted.yml | 4 ++-- .github/workflows/unit-tests.yml | 2 +- .github/workflows/verify-gha-unit-tests-count.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit-tests-gh-hosted.yml b/.github/workflows/unit-tests-gh-hosted.yml index fd01feeccd99..94473111404d 100644 --- a/.github/workflows/unit-tests-gh-hosted.yml +++ b/.github/workflows/unit-tests-gh-hosted.yml @@ -9,7 +9,7 @@ on: jobs: run-test: - if: github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private' + if: (github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == true)) runs-on: ubuntu-20.04 strategy: fail-fast: false @@ -75,7 +75,7 @@ jobs: uses: ./.github/actions/unit-tests collect-and-verify: - if: github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private' + if: (github.repository != 'openedx/edx-platform' && github.repository != 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == true)) runs-on: ubuntu-20.04 strategy: matrix: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 51ae55569146..66cb5f7fd13f 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -9,7 +9,7 @@ on: jobs: run-tests: name: python-${{ matrix.python-version }},django-${{ matrix.django-version }},${{ matrix.shard_name }} - if: github.repository == 'openedx/edx-platform' || github.repository == 'edx/edx-platform-private' + if: (github.repository == 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == false)) runs-on: [ edx-platform-runner ] strategy: matrix: diff --git a/.github/workflows/verify-gha-unit-tests-count.yml b/.github/workflows/verify-gha-unit-tests-count.yml index 039d45ad9548..c68092942d70 100644 --- a/.github/workflows/verify-gha-unit-tests-count.yml +++ b/.github/workflows/verify-gha-unit-tests-count.yml @@ -8,7 +8,7 @@ on: jobs: collect-and-verify: - if: github.repository == 'openedx/edx-platform' || github.repository == 'edx/edx-platform-private' + if: (github.repository == 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == false)) runs-on: [ edx-platform-runner ] steps: - name: sync directory owner From df4f843adaf7e67138334ebb31450f9ed2f8e651 Mon Sep 17 00:00:00 2001 From: Soban Javed Date: Tue, 22 Nov 2022 02:14:00 +0500 Subject: [PATCH 030/519] build: update shard in unit test worflow for forks This workflow wasn't updated when we updated shards for self-hosted runners, so updating in this commit --- .github/workflows/unit-tests-gh-hosted.yml | 2 +- .github/workflows/unit-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/unit-tests-gh-hosted.yml b/.github/workflows/unit-tests-gh-hosted.yml index 94473111404d..5edfbff88f9d 100644 --- a/.github/workflows/unit-tests-gh-hosted.yml +++ b/.github/workflows/unit-tests-gh-hosted.yml @@ -31,7 +31,7 @@ jobs: "cms-2", "common-1", "common-2", - "common-3", + "xmodule-1" ] name: gh-hosted-python-${{ matrix.python-version }},django-${{ matrix.django-version }},${{ matrix.shard_name }} steps: diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 66cb5f7fd13f..50b896977391 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -80,7 +80,7 @@ jobs: # https://github.com/orgs/community/discussions/33579 success: name: Tests successful - if: always() + if: (github.repository == 'edx/edx-platform-private') || (github.repository == 'openedx/edx-platform' && (startsWith(github.base_ref, 'open-release') == false)) needs: - run-tests runs-on: ubuntu-latest From 39b7b2b426c8b60318e1821b8c6e6d39f92c7410 Mon Sep 17 00:00:00 2001 From: Syed Sajjad Hussain Shah Date: Thu, 17 Nov 2022 08:43:40 +0500 Subject: [PATCH 031/519] fix: XSS and open redirect vulnerability --- openedx/core/djangoapps/user_authn/utils.py | 10 ++++++++++ .../djangoapps/user_authn/views/tests/test_logout.py | 2 ++ 2 files changed, 12 insertions(+) diff --git a/openedx/core/djangoapps/user_authn/utils.py b/openedx/core/djangoapps/user_authn/utils.py index 490223eaecda..5b70d2427859 100644 --- a/openedx/core/djangoapps/user_authn/utils.py +++ b/openedx/core/djangoapps/user_authn/utils.py @@ -19,6 +19,13 @@ from openedx.core.djangoapps.user_api.accounts import USERNAME_MAX_LENGTH +def _remove_unsafe_bytes_from_url(url): + _UNSAFE_URL_BYTES_TO_REMOVE = ["\t", "\r", "\n"] + for byte in _UNSAFE_URL_BYTES_TO_REMOVE: + url = url.replace(byte, "") + return url + + def is_safe_login_or_logout_redirect(redirect_to, request_host, dot_client_id, require_https): """ Determine if the given redirect URL/path is safe for redirection. @@ -41,6 +48,8 @@ def is_safe_login_or_logout_redirect(redirect_to, request_host, dot_client_id, r login_redirect_whitelist = set(getattr(settings, 'LOGIN_REDIRECT_WHITELIST', [])) login_redirect_whitelist.add(request_host) + redirect_to = _remove_unsafe_bytes_from_url(redirect_to) + # Allow OAuth2 clients to redirect back to their site after logout. if dot_client_id: application = Application.objects.get(client_id=dot_client_id) @@ -50,6 +59,7 @@ def is_safe_login_or_logout_redirect(redirect_to, request_host, dot_client_id, r is_safe_url = http.is_safe_url( redirect_to, allowed_hosts=login_redirect_whitelist, require_https=require_https ) + return is_safe_url diff --git a/openedx/core/djangoapps/user_authn/views/tests/test_logout.py b/openedx/core/djangoapps/user_authn/views/tests/test_logout.py index 2a458cb4fcea..84fa3b8d7d1d 100644 --- a/openedx/core/djangoapps/user_authn/views/tests/test_logout.py +++ b/openedx/core/djangoapps/user_authn/views/tests/test_logout.py @@ -88,6 +88,8 @@ def test_no_redirect_supplied(self): @ddt.data( ('https://www.amazon.org', 'edx.org'), + ('/%09/google.com/', 'edx.org'), + ('java%0D%0Ascript%0D%0A%3aalert(document.domain)', 'edx.org'), ) @ddt.unpack def test_logout_redirect_failure(self, redirect_url, host): From 203bf9fc30df005a75c44e6e6335302b9ae80dfe Mon Sep 17 00:00:00 2001 From: connorhaugh <49422820+connorhaugh@users.noreply.github.com> Date: Mon, 28 Nov 2022 04:21:44 -0500 Subject: [PATCH 032/519] fix: studio submit handler (olive backport) (#31219) * fix: studio submit handler * style: remove extra line Co-authored-by: Matjaz Gregoric --- cms/djangoapps/contentstore/views/component.py | 5 +++++ cms/djangoapps/contentstore/views/tests/test_item.py | 10 ++++++++++ 2 files changed, 15 insertions(+) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index da15dc5c84dc..d20cab68d74a 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -538,6 +538,11 @@ def component_handler(request, usage_key_string, handler, suffix=''): """ usage_key = UsageKey.from_string(usage_key_string) + # Addendum: + # TNL 101-62 studio write permission is also checked for editing content. + + if handler == 'submit_studio_edits' and not has_course_author_access(request.user, usage_key.course_key): + raise PermissionDenied("No studio write Permissions") # Let the module handle the AJAX req = django_to_webob_request(request) diff --git a/cms/djangoapps/contentstore/views/tests/test_item.py b/cms/djangoapps/contentstore/views/tests/test_item.py index 055cea6401c2..cf7ef1ecc3a8 100644 --- a/cms/djangoapps/contentstore/views/tests/test_item.py +++ b/cms/djangoapps/contentstore/views/tests/test_item.py @@ -8,6 +8,7 @@ import ddt from django.conf import settings +from django.core.exceptions import PermissionDenied from django.http import Http404 from django.test import TestCase from django.test.client import RequestFactory @@ -2197,6 +2198,15 @@ def create_response(handler, request, suffix): # lint-amnesty, pylint: disable= self.assertEqual(component_handler(self.request, self.usage_key_string, 'dummy_handler').status_code, status_code) + def test_submit_studio_edits_checks_author_permission(self): + with self.assertRaises(PermissionDenied): + with patch( + 'common.djangoapps.student.auth.has_course_author_access', + return_value=False + ) as mocked_has_course_author_access: + component_handler(self.request, self.usage_key_string, 'submit_studio_edits') + assert mocked_has_course_author_access.called is True + @ddt.data((True, True), (False, False),) @ddt.unpack def test_aside(self, is_xblock_aside, is_get_aside_called): From 903ea00a124680100a96f91c9735a71f35cce065 Mon Sep 17 00:00:00 2001 From: Agrendalath Date: Mon, 28 Nov 2022 16:14:31 +0100 Subject: [PATCH 033/519] feat!: update Drag and Drop v2 XBlock to prevent XSS vulnerabilities BREAKING CHANGE: disallowed HTML tags (e.g. + + % endif diff --git a/lms/templates/main.html b/lms/templates/main.html index 9ee3b6d5b13e..ac43e6eba25f 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -162,6 +162,18 @@ % endif +<% ga_4_id = static.get_value("GOOGLE_ANALYTICS_4_ID", settings.GOOGLE_ANALYTICS_4_ID) %> +% if ga_4_id: + + +% endif + <% branch_key = static.get_value("BRANCH_IO_KEY", settings.BRANCH_IO_KEY) %> % if branch_key and not is_from_mobile_app: + + {% endif %} + diff --git a/openedx/core/djangoapps/site_configuration/templatetags/configuration.py b/openedx/core/djangoapps/site_configuration/templatetags/configuration.py index b5242a53b5b9..9de10d3bb07b 100644 --- a/openedx/core/djangoapps/site_configuration/templatetags/configuration.py +++ b/openedx/core/djangoapps/site_configuration/templatetags/configuration.py @@ -65,3 +65,12 @@ def microsite_template_path(template_name): """ template_name = theming_helpers.get_template_path(template_name) return template_name[1:] if template_name[0] == '/' else template_name + + +@register.simple_tag +def google_analytics_4_id(): + """ + Django template tag that outputs the GOOGLE_ANALYTICS_4_ID: + {% google_analytics_4_id %} + """ + return configuration_helpers.get_value("GOOGLE_ANALYTICS_4_ID", settings.GOOGLE_ANALYTICS_4_ID) From a8adccf7a04024dbe11a73ceab9a61f0074a306e Mon Sep 17 00:00:00 2001 From: Matjaz Gregoric Date: Thu, 18 May 2023 20:36:47 +0200 Subject: [PATCH 063/519] chore: upgrade Django to 3.2.19 --- requirements/edx/base.txt | 2 +- requirements/edx/development.txt | 2 +- requirements/edx/django.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt index 878b39cb86a9..e3320f534b1c 100644 --- a/requirements/edx/base.txt +++ b/requirements/edx/base.txt @@ -178,7 +178,7 @@ deprecated==1.2.13 # via # jwcrypto # redis -django==3.2.18 +django==3.2.19 # via # -c requirements/edx/../common_constraints.txt # -r requirements/edx/base.in diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt index 7acaab8b592e..3ab87414d38f 100644 --- a/requirements/edx/development.txt +++ b/requirements/edx/development.txt @@ -267,7 +267,7 @@ distlib==0.3.6 # via # -r requirements/edx/testing.txt # virtualenv -django==3.2.18 +django==3.2.19 # via # -c requirements/edx/../common_constraints.txt # -r requirements/edx/testing.txt diff --git a/requirements/edx/django.txt b/requirements/edx/django.txt index a244e49c7977..0e0a45235358 100644 --- a/requirements/edx/django.txt +++ b/requirements/edx/django.txt @@ -1 +1 @@ -django==3.2.18 +django==3.2.19 From 0bb47557a12e4e0ba6ff338a63fd7288eb688ea3 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Sun, 4 Jun 2023 11:32:11 +0700 Subject: [PATCH 064/519] merge --- lms/djangoapps/courseware/module_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 6951beae305a..c8378b2be973 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -106,7 +106,6 @@ from xmodule.exceptions import NotFoundError, ProcessingError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.util.sandboxing import can_execute_unsafe_code, get_python_lib_zip # lint-amnesty, pylint: disable=wrong-import-order from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary From f42e11f3a8ea35522ca5addc68f268e3c4156f7b Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Sun, 4 Jun 2023 11:37:13 +0700 Subject: [PATCH 065/519] =?UTF-8?q?remove=20course=5Fhome=5Flegacy=5F?= =?UTF-8?q?=C3=AD=5Factivate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openedx/features/funix_relative_date/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py index 5c574ac90b13..36ff2b536d22 100644 --- a/openedx/features/funix_relative_date/views.py +++ b/openedx/features/funix_relative_date/views.py @@ -14,7 +14,7 @@ from common.djangoapps.student.models import CourseEnrollment from lms.djangoapps.course_goals.models import UserActivity from lms.djangoapps.course_home_api.dates.serializers import DatesTabSerializer -from lms.djangoapps.course_home_api.toggles import course_home_legacy_is_active + from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access From 70e915cacf313ee74d12b3ac292591e60cee8653 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Sun, 4 Jun 2023 12:10:05 +0700 Subject: [PATCH 066/519] =?UTF-8?q?remove=20course=5Fhome=5Flegacy=5F?= =?UTF-8?q?=C3=AD=5Factivate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openedx/features/funix_relative_date/views.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py index 36ff2b536d22..8a63fe04423e 100644 --- a/openedx/features/funix_relative_date/views.py +++ b/openedx/features/funix_relative_date/views.py @@ -38,8 +38,7 @@ def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) - if course_home_legacy_is_active(course_key): - raise Http404 + # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) From a75c68471c36deebf634ced87567d4b998fce87a Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Sun, 4 Jun 2023 13:10:44 +0700 Subject: [PATCH 067/519] as --- openedx/features/funix_relative_date/funix_relative_date.py | 2 +- openedx/features/funix_relative_date/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index eb13a9efc82d..625533aeb459 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -22,7 +22,7 @@ def get_course_date_blocks(self, course, user, request=None): date_blocks = FunixRelativeDateDAO.get_all_block_by_id(user_id=user.id, course_id=course.id) date_blocks = list(date_blocks) date_blocks.sort(key=lambda x: x.index) - + print('---------------date_block:', date_blocks) # Add start date start_date = date_blocks.pop(0) output = [ diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py index 8a63fe04423e..3b13010ee06f 100644 --- a/openedx/features/funix_relative_date/views.py +++ b/openedx/features/funix_relative_date/views.py @@ -12,12 +12,12 @@ from rest_framework.response import Response from common.djangoapps.student.models import CourseEnrollment -from lms.djangoapps.course_goals.models import UserActivity + from lms.djangoapps.course_home_api.dates.serializers import DatesTabSerializer from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs -from lms.djangoapps.courseware.courses import get_course_date_blocks, get_course_with_access +from lms.djangoapps.courseware.courses import get_course_with_access from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary from lms.djangoapps.courseware.date_summary import TodaysDate from lms.djangoapps.courseware.masquerade import setup_masquerade From 10fc96f7c75738ee6fa73049dd50773cdc4156e7 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Sun, 4 Jun 2023 13:23:46 +0700 Subject: [PATCH 068/519] a --- openedx/features/funix_relative_date/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py index 4d9a93e0952d..a797b69c08d0 100644 --- a/openedx/features/funix_relative_date/models.py +++ b/openedx/features/funix_relative_date/models.py @@ -28,6 +28,10 @@ def delete_all_date(self, user_id, course_id): @classmethod def get_all_block_by_id(self, user_id, course_id): + print('-----------get_all_block_by_id:',FunixRelativeDate.objects.filter( + user_id=user_id, + course_id=course_id + ) ) return FunixRelativeDate.objects.filter( user_id=user_id, course_id=course_id From af3d39a6e71cf897673e1d22d515aad109e4586a Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Mon, 5 Jun 2023 12:03:05 +0700 Subject: [PATCH 069/519] get & filter --- openedx/features/funix_relative_date/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py index a797b69c08d0..014c95b2962b 100644 --- a/openedx/features/funix_relative_date/models.py +++ b/openedx/features/funix_relative_date/models.py @@ -39,7 +39,7 @@ def get_all_block_by_id(self, user_id, course_id): @classmethod def get_enroll_by_id(self, user_id, course_id): - return FunixRelativeDate.objects.get(user_id=user_id, course_id=course_id, type="start") + return FunixRelativeDate.objects.filter(user_id=user_id, course_id=course_id, type="start")[0] @classmethod def get_all_enroll_by_course(self, course_id): From 8e6e9875de87cabb859c8cd774fbca846a6309d2 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:12:57 +0700 Subject: [PATCH 070/519] fix FUNiXDatesTabSerializer --- openedx/features/funix_relative_date/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py index 77ea152e78a9..7e97233457c7 100644 --- a/openedx/features/funix_relative_date/views.py +++ b/openedx/features/funix_relative_date/views.py @@ -18,6 +18,7 @@ from lms.djangoapps.courseware.access import has_access from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs from lms.djangoapps.courseware.courses import get_course_with_access +from openedx.features.funix_relative_date.serializers import FUNiXDatesTabSerializer from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary from lms.djangoapps.courseware.date_summary import TodaysDate from lms.djangoapps.courseware.masquerade import setup_masquerade From 6f2bef9bec62c79c3e0d6403599e2d790bfd727b Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:15:43 +0700 Subject: [PATCH 071/519] fix import funix_relative_date --- lms/djangoapps/courseware/module_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index c8378b2be973..b79d855f0c6e 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -106,7 +106,7 @@ from xmodule.exceptions import NotFoundError, ProcessingError # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.django import modulestore # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.exceptions import ItemNotFoundError # lint-amnesty, pylint: disable=wrong-import-order -from openedx.features.funix_relative_date.funix_relative_date import FunixRelativeDateLibary +from openedx.features.funix_relative_date import funix_relative_date log = logging.getLogger(__name__) @@ -576,7 +576,7 @@ def handle_completion_event(block, event): block_key=block.scope_ids.usage_id, completion=event['completion'], ) - FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) + funix_relative_date.FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) def handle_grade_event(block, event): """ @@ -621,7 +621,7 @@ def handle_deprecated_progress_event(block, event): block_key=block.scope_ids.usage_id, completion=1.0, ) - FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) + funix_relative_date.FunixRelativeDateLibary.get_schedule(user_name=str(user), course_id=str(course_id)) # Rebind module service to deal with noauth modules getting attached to users rebind_user_service = RebindUserService( From 35939272d786d24305f749d9cb351f2365c73cd1 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Mon, 5 Jun 2023 13:18:16 +0700 Subject: [PATCH 072/519] fix import LearnGoal --- openedx/features/funix_relative_date/funix_relative_date.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index 563ee5cae4cb..b02e8f25a77a 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -8,7 +8,7 @@ from openedx.features.funix_relative_date.models import FunixRelativeDate, FunixRelativeDateDAO from opaque_keys.edx.keys import CourseKey from lms.djangoapps.courseware.date_summary import FunixCourseStartDate, TodaysDate -from openedx.features.funix_goal.models import LearnGoal +from openedx.features.funix_goal import models class FunixRelativeDateLibary(): TIME_PER_DAY = 2.5 * 60 @@ -84,7 +84,7 @@ def get_time(last_complete_date, goal, day=1): FunixRelativeDateDAO.delete_all_date(user_id=user.id, course_id=course_id) # Get goal - goal = LearnGoal.get_goal(course_id=course_id, user_id=str(user.id)) + goal = models.LearnGoal.get_goal(course_id=course_id, user_id=str(user.id)) index = 0 completed_assignments = [asm for asm in assignment_blocks if asm.complete] uncompleted_assignments = [asm for asm in assignment_blocks if not asm.complete] From 32a031a8704dacdb1f9fcdfa0ccb9cba6b57e550 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 10:46:13 +0700 Subject: [PATCH 073/519] merge FEAT/Hannah_View_Date --- lms/djangoapps/course_home_api/urls.py | 5 +++ lms/djangoapps/instructor/views/api.py | 32 ++++++++++++++++ lms/djangoapps/instructor/views/api_urls.py | 1 + .../instructor/views/instructor_dashboard.py | 4 ++ .../js/instructor_dashboard/student_admin.js | 33 ++++++++++++++++ .../instructor_dashboard_2/student_admin.html | 18 +++++++++ openedx/features/funix_goal/views.py | 20 +++++++++- .../funix_relative_date/serializers.py | 1 + openedx/features/funix_relative_date/views.py | 38 ++++++++++++++----- 9 files changed, 141 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/course_home_api/urls.py b/lms/djangoapps/course_home_api/urls.py index 19b70fe7beaf..027769b56807 100644 --- a/lms/djangoapps/course_home_api/urls.py +++ b/lms/djangoapps/course_home_api/urls.py @@ -50,6 +50,11 @@ # Funix Dates Tab URLs urlpatterns += [ + re_path( + fr'dates-funix/{settings.COURSE_KEY_PATTERN}/(?P[^/]+)', + FunixRelativeDatesTabView.as_view(), + name='funix-dates-tab-other-student' + ), re_path( fr'dates-funix/{settings.COURSE_KEY_PATTERN}', FunixRelativeDatesTabView.as_view(), diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 8388fffb4fcf..fd15972ebe6a 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -3602,3 +3602,35 @@ def _get_branded_email_template(course_overview): template_name = template_name.get(course_overview.display_org_with_default) return template_name + + + +@require_POST +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +@require_course_permission(permissions.ENROLLMENT_REPORT) +@require_post_params( + unique_student_identifier="email or username of student for whom to get progress url" +) +@common_exceptions_400 +def get_student_dates_url(request, course_id): + """ + Get the dates url of a student. + Limited to staff access. + Takes query parameter unique_student_identifier and if the student exists + returns e.g. { + 'dates_url': '/../...' + } + """ + course_id = CourseKey.from_string(course_id) + user = get_student_from_identifier(request.POST.get('unique_student_identifier')) + + # only use mfe url + dates_url = get_learning_mfe_home_url(course_id, url_fragment='dates') + if user is not None: + dates_url += '/{}/'.format(user.id) + response_payload = { + 'course_id': str(course_id), + 'dates_url': dates_url, + } + return JsonResponse(response_payload) \ No newline at end of file diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py index 0b4a88d1b7c6..8d38522bd1aa 100644 --- a/lms/djangoapps/instructor/views/api_urls.py +++ b/lms/djangoapps/instructor/views/api_urls.py @@ -88,4 +88,5 @@ path('generate_bulk_certificate_exceptions', api.generate_bulk_certificate_exceptions, name='generate_bulk_certificate_exceptions'), path('certificate_invalidation_view/', api.certificate_invalidation_view, name='certificate_invalidation_view'), + path('get_student_dates_url', api.get_student_dates_url, name='get_student_dates_url'), ] diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 4625402f6efc..586a94973bee 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -572,6 +572,10 @@ def _section_student_admin(course, access): kwargs={'course_id': str(course_key)} ), 'spoc_gradebook_url': reverse('spoc_gradebook', kwargs={'course_id': str(course_key)}), + 'get_student_dates_url_url': reverse( + 'get_student_dates_url', + kwargs={'course_id': str(course_key)} + ), } if is_writable_gradebook_enabled(course_key) and settings.WRITABLE_GRADEBOOK_URL: section_data['writable_gradebook_url'] = f'{settings.WRITABLE_GRADEBOOK_URL}/{str(course_key)}' diff --git a/lms/static/js/instructor_dashboard/student_admin.js b/lms/static/js/instructor_dashboard/student_admin.js index 54b773faaae6..f314398627d7 100644 --- a/lms/static/js/instructor_dashboard/student_admin.js +++ b/lms/static/js/instructor_dashboard/student_admin.js @@ -35,8 +35,10 @@ this.$field_student_select_enrollment_status = findAndAssert(this.$section, "input[name='student-select-enrollment-status']"); this.$field_student_select_progress = findAndAssert(this.$section, "input[name='student-select-progress']"); this.$field_student_select_grade = findAndAssert(this.$section, "input[name='student-select-grade']"); + this.$field_student_select_dates = findAndAssert(this.$section, "input[name='student-select-dates']"); this.$progress_link = findAndAssert(this.$section, 'a.progress-link'); this.$field_problem_select_single = findAndAssert(this.$section, "input[name='problem-select-single']"); + this.$dates_link = findAndAssert(this.$section, 'a.dates-link'); this.$btn_reset_attempts_single = findAndAssert(this.$section, "input[name='reset-attempts-single']"); this.$btn_delete_state_single = this.$section.find("input[name='delete-state-single']"); this.$btn_rescore_problem_single = this.$section.find("input[name='rescore-problem-single']"); @@ -69,6 +71,7 @@ this.$request_err_enrollment_status = findAndAssert(this.$section, '.student-enrollment-status-container .request-response-error'); this.$request_err_progress = findAndAssert(this.$section, '.student-progress-container .request-response-error'); this.$request_err_grade = findAndAssert(this.$section, '.student-grade-container .request-response-error'); + this.$request_err_dates = findAndAssert(this.$section, '.student-dates-container .request-response-error'); this.$request_err_ee = this.$section.find('.entrance-exam-grade-container .request-response-error'); this.$request_response_error_all = this.$section.find('.course-specific-container .request-response-error'); this.$enrollment_status_link = findAndAssert(this.$section, 'a.enrollment-status-link'); @@ -134,6 +137,36 @@ }) }); }); + this.$dates_link.click(function(e) { + var errorMessage, fullErrorMessage, uniqStudentIdentifier; + e.preventDefault(); + uniqStudentIdentifier = studentadmin.$field_student_select_dates.val(); + if (!uniqStudentIdentifier) { + return studentadmin.$request_err_dates.text( + gettext('Please enter a student email address or username.') + ); + } + errorMessage = gettext("Error getting student progress url for '<%- student_id %>'. Make sure that the student identifier is spelled correctly."); // eslint-disable-line max-len + fullErrorMessage = _.template(errorMessage)({ + student_id: uniqStudentIdentifier + }); + return $.ajax({ + type: 'POST', + dataType: 'json', + url: studentadmin.$dates_link.data('endpoint'), + data: { + unique_student_identifier: uniqStudentIdentifier + }, + success: studentadmin.clear_errors_then(function(data) { + window.location = data.dates_url; + return window.location; + }), + error: statusAjaxError(function() { + return studentadmin.$request_err_dates.text(fullErrorMessage); + }) + }); + }); + this.$btn_reset_attempts_single.click(function() { var errorMessage, fullErrorMessage, fullSuccessMessage, problemToReset, sendData, successMessage, uniqStudentIdentifier; diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html index ec21e3a99316..31eb1759ccae 100644 --- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html +++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html @@ -58,6 +58,24 @@

${_("View a specific learner's grades and progress")}


+
+

${_("View a specific learner's dates")}

+
+ +
+ +

+ +
+

${_("Adjust a learner's grade for a specific problem")}

diff --git a/openedx/features/funix_goal/views.py b/openedx/features/funix_goal/views.py index 975d0d7453a6..c744eb38525d 100644 --- a/openedx/features/funix_goal/views.py +++ b/openedx/features/funix_goal/views.py @@ -5,16 +5,32 @@ from rest_framework.response import Response from rest_framework.decorators import api_view, authentication_classes, permission_classes # lint-amnesty, pylint: disable=wrong-import-order from openedx.features.funix_goal.models import LearnGoal +from django.contrib.auth import get_user_model +from django.http.response import Http404 +User = get_user_model() + @api_view(['POST']) @authentication_classes((JwtAuthentication,)) @permission_classes((IsAuthenticated,)) def set_goal(request): + def _get_student(request, target_user_id): + if target_user_id is None: + return request.user + try: + return User.objects.get(id=target_user_id) + except User.DoesNotExist as exc: + raise Http404 from exc + course_id = request.data.get('course_id') hours_per_day = float(request.data.get('hours_per_day')) week_days = list(request.data.get('week_days')) + target_user_id = request.data.get('target_user_id') + + target_user_id = int(target_user_id) if target_user_id is not None else None + - LearnGoal.set_goal(course_id=course_id, user=request.user, hours_per_day=hours_per_day, week_days=week_days) + LearnGoal.set_goal(course_id=course_id, user=_get_student(request, target_user_id), hours_per_day=hours_per_day, week_days=week_days) - return Response(status=202) + return Response(status=202) \ No newline at end of file diff --git a/openedx/features/funix_relative_date/serializers.py b/openedx/features/funix_relative_date/serializers.py index eb9dfc6e5f22..655ecfe82688 100644 --- a/openedx/features/funix_relative_date/serializers.py +++ b/openedx/features/funix_relative_date/serializers.py @@ -8,4 +8,5 @@ class FUNiXDatesTabSerializer(DatesTabSerializer): Serializer for the FUNiX Dates Tab """ goal_hours_per_day = serializers.FloatField() + username = serializers.CharField() goal_weekdays = serializers.ListField(child=serializers.BooleanField()) diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py index 7e97233457c7..45be8237e1f0 100644 --- a/openedx/features/funix_relative_date/views.py +++ b/openedx/features/funix_relative_date/views.py @@ -25,6 +25,9 @@ from openedx.core.lib.api.authentication import BearerAuthenticationAllowInactiveUser from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.funix_goal.models import LearnGoal +from django.contrib.auth import get_user_model + +User = get_user_model() class FunixRelativeDatesTabView(RetrieveAPIView): @@ -35,11 +38,32 @@ class FunixRelativeDatesTabView(RetrieveAPIView): ) permission_classes = (IsAuthenticated,) serializer_class = FUNiXDatesTabSerializer + def _get_student_user(self, request, course_key, student_id, is_staff): + if student_id: + try: + student_id = int(student_id) + except ValueError as e: + raise Http404 from e + + if student_id is None or student_id == request.user.id: + _, student = setup_masquerade( + request, + course_key, + staff_access=is_staff, + reset_masquerade_data=True + ) + return student + + try: + return User.objects.get(id=student_id) + except User.DoesNotExist as exc: + raise Http404 from exc + def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) - + student_id = kwargs.get('student_id') # Enable NR tracing for this view based on course @@ -50,13 +74,8 @@ def get(self, request, *args, **kwargs): course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) is_staff = bool(has_access(request.user, 'staff', course_key)) - _, request.user = setup_masquerade( - request, - course_key, - staff_access=is_staff, - reset_masquerade_data=True, - ) - + request.user = self._get_student_user(request, course_key, student_id, is_staff) + if not CourseEnrollment.is_enrolled(request.user, course_key) and not is_staff: return Response('User not enrolled.', status=401) @@ -80,7 +99,8 @@ def get(self, request, *args, **kwargs): 'learner_is_full_access': learner_is_full_access, 'user_timezone': user_timezone, 'goal_hours_per_day': goal.hours_per_day, - 'goal_weekdays': [getattr(goal, f'weekday_{i}') for i in range(7)] + 'goal_weekdays': [getattr(goal, f'weekday_{i}') for i in range(7)], + 'username': request.user.username } context = self.get_serializer_context() context['learner_is_full_access'] = learner_is_full_access From 26b2e2036fbd72c88bbf268f342e8e81b3c2185c Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 11:05:54 +0700 Subject: [PATCH 074/519] [Add] mark_user_change_as_expected to funix dates --- openedx/features/funix_relative_date/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openedx/features/funix_relative_date/views.py b/openedx/features/funix_relative_date/views.py index 45be8237e1f0..e7a0dfcb0fa0 100644 --- a/openedx/features/funix_relative_date/views.py +++ b/openedx/features/funix_relative_date/views.py @@ -26,6 +26,7 @@ from openedx.features.content_type_gating.models import ContentTypeGatingConfig from openedx.features.funix_goal.models import LearnGoal from django.contrib.auth import get_user_model +from openedx.core.djangoapps.safe_sessions.middleware import mark_user_change_as_expected User = get_user_model() @@ -105,5 +106,5 @@ def get(self, request, *args, **kwargs): context = self.get_serializer_context() context['learner_is_full_access'] = learner_is_full_access serializer = self.get_serializer_class()(data, context=context) - + mark_user_change_as_expected(request.user.id) return Response(serializer.data) From 1b772c5cc1f60b744de386e5a78899242b3b8265 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 12:00:50 +0700 Subject: [PATCH 075/519] [ADD] Add instructor_tools_dashboard --- lms/djangoapps/instructor_tools/__init__.py | 0 lms/djangoapps/instructor_tools/apps.py | 6 ++++ lms/djangoapps/instructor_tools/views.py | 10 ++++++ lms/envs/common.py | 3 ++ .../instructor_tools_dashboard.html | 31 +++++++++++++++++++ lms/urls.py | 3 ++ 6 files changed, 53 insertions(+) create mode 100644 lms/djangoapps/instructor_tools/__init__.py create mode 100644 lms/djangoapps/instructor_tools/apps.py create mode 100644 lms/djangoapps/instructor_tools/views.py create mode 100644 lms/templates/instructor_tools/instructor_tools_dashboard.html diff --git a/lms/djangoapps/instructor_tools/__init__.py b/lms/djangoapps/instructor_tools/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/lms/djangoapps/instructor_tools/apps.py b/lms/djangoapps/instructor_tools/apps.py new file mode 100644 index 000000000000..c4e18801e9f6 --- /dev/null +++ b/lms/djangoapps/instructor_tools/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class InstructorToolsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'lms.djangoapps.instructor_tools' \ No newline at end of file diff --git a/lms/djangoapps/instructor_tools/views.py b/lms/djangoapps/instructor_tools/views.py new file mode 100644 index 000000000000..39fd29a19c9f --- /dev/null +++ b/lms/djangoapps/instructor_tools/views.py @@ -0,0 +1,10 @@ +from django.contrib.auth.decorators import login_required +from django.views.decorators.csrf import ensure_csrf_cookie +from common.djangoapps.edxmako.shortcuts import render_to_response + +@login_required +@ensure_csrf_cookie +def instructor_tools_dashboard(request): + response = render_to_response('instructor_tools/instructor_tools_dashboard.html', {}) + + return response \ No newline at end of file diff --git a/lms/envs/common.py b/lms/envs/common.py index 23cd623d6fd3..9ff0be13cdd7 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -3255,6 +3255,9 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring # MFE API 'lms.djangoapps.mfe_config_api', + + # instructor_tools + 'lms.djangoapps.instructor_tools' ] ######################### CSRF ######################################### diff --git a/lms/templates/instructor_tools/instructor_tools_dashboard.html b/lms/templates/instructor_tools/instructor_tools_dashboard.html new file mode 100644 index 000000000000..e38f23f9109a --- /dev/null +++ b/lms/templates/instructor_tools/instructor_tools_dashboard.html @@ -0,0 +1,31 @@ +<%page expression_filter="h"/> +<%inherit file="../main.html" /> +<%def name="online_help_token()"><% return "instructor_tools_dashboard" %> + +<%! + +%> + +<%block name="headextra"> + + + +
+
+
+
+
+

Instructor Tools

+ +
+
+

Export all grades

+

+

+
+ +
+
+
+
+
\ No newline at end of file diff --git a/lms/urls.py b/lms/urls.py index f02b1ecadb89..53f64814561c 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -50,6 +50,7 @@ from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.user_authn.views.login import redirect_to_lms_login from openedx.features.enterprise_support.api import enterprise_enabled +from lms.djangoapps.instructor_tools import views as instructor_tools_views RESET_COURSE_DEADLINES_NAME = 'reset_course_deadlines' RENDER_XBLOCK_NAME = 'render_xblock' @@ -98,6 +99,8 @@ path('', include('common.djangoapps.student.urls')), # TODO: Move lms specific student views out of common code re_path(r'^dashboard/?$', student_views.student_dashboard, name='dashboard'), + + path('instructor_tools', instructor_tools_views.instructor_tools_dashboard, name='instructor_tools_dashboard'), path('change_enrollment', student_views.change_enrollment, name='change_enrollment'), # Event tracking endpoints From 7b484a466b5b414044161f1a5450ea314aa376ec Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 12:21:39 +0700 Subject: [PATCH 076/519] [ADD] Add get_all_csv_file + [ADD] Add export all grades as CSV API --- lms/djangoapps/instructor_task/api.py | 11 +++++++ lms/djangoapps/instructor_task/api_helper.py | 17 +++++++++++ .../instructor_tools/all_grades_CSV.py | 29 +++++++++++++++++++ lms/djangoapps/instructor_tools/api.py | 16 ++++++++++ lms/djangoapps/instructor_tools/urls.py | 10 +++++++ .../instructor_tools_dashboard.js | 15 ++++++++++ .../instructor_tools_dashboard.html | 6 +++- lms/urls.py | 2 ++ 8 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 lms/djangoapps/instructor_tools/all_grades_CSV.py create mode 100644 lms/djangoapps/instructor_tools/api.py create mode 100644 lms/djangoapps/instructor_tools/urls.py create mode 100644 lms/static/js/instructor_tools/instructor_tools_dashboard.js diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index bbf5bf7b2879..e24a755ac603 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -26,6 +26,7 @@ encode_problem_and_student_input, schedule_task, submit_task, + submit_task_not_async, submit_scheduled_task, ) from lms.djangoapps.instructor_task.data import InstructorTaskTypes @@ -366,6 +367,16 @@ def submit_calculate_grades_csv(request, course_key, **task_kwargs): task_key = "" return submit_task(request, task_type, task_class, course_key, task_input, task_key) + +def submit_calculate_grades_csv_not_async(request, course_key, **task_kwargs): + task_type = 'grade_course' + task_class = calculate_grades_csv + task_input = task_kwargs + task_key = "" + + return submit_task_not_async(request, task_type, task_class, course_key, task_input, task_key) + + def submit_problem_grade_report(request, course_key, **task_kwargs): diff --git a/lms/djangoapps/instructor_task/api_helper.py b/lms/djangoapps/instructor_task/api_helper.py index b346b9700d44..5e2baa1787b8 100644 --- a/lms/djangoapps/instructor_task/api_helper.py +++ b/lms/djangoapps/instructor_task/api_helper.py @@ -469,6 +469,23 @@ def submit_task(request, task_type, task_class, course_key, task_input, task_key return instructor_task +def submit_task_not_async(request, task_type, task_class, course_key, task_input, task_key): + with outer_atomic(): + # check to see if task is already running, and reserve it otherwise: + instructor_task = _reserve_task(course_key, task_type, task_key, task_input, request.user) + + # make sure all data has been committed before handing off task to celery. + + task_id = instructor_task.task_id + task_args = [instructor_task.id, _get_xmodule_instance_args(request, task_id)] + try: + task_class.apply(task_args, task_id=task_id) + + except Exception as error: # lint-amnesty, pylint: disable=broad-except + _handle_instructor_task_failure(instructor_task, error) + + return instructor_task + def schedule_task(request, task_type, course_key, task_input, task_key, schedule): """ diff --git a/lms/djangoapps/instructor_tools/all_grades_CSV.py b/lms/djangoapps/instructor_tools/all_grades_CSV.py new file mode 100644 index 000000000000..ca59dc250b31 --- /dev/null +++ b/lms/djangoapps/instructor_tools/all_grades_CSV.py @@ -0,0 +1,29 @@ +import os + +directory = '/edx/var/edxapp/uploads/' + +def get_all_csv_file(): + all_dir = [x for x in os.walk(directory)] + all_dir.pop(0) + + # all_dir = list(map(lambda x: x[2], all_dir)) + + res = [] + + for x in all_dir: + root_path = x[0] + + # filter _grade_report_ file in x[2] + files = list(filter(lambda x: '_grade_report_' in x, x[2])) + + # Sort x[2] by name reverse + files.sort(reverse=True) + + # get the latest file + latest_file = files[0] + + # get the latest file path + latest_file_path = os.path.join(root_path, latest_file) + res.append(latest_file_path) + + return res \ No newline at end of file diff --git a/lms/djangoapps/instructor_tools/api.py b/lms/djangoapps/instructor_tools/api.py new file mode 100644 index 000000000000..ad84b20b1f69 --- /dev/null +++ b/lms/djangoapps/instructor_tools/api.py @@ -0,0 +1,16 @@ +from django.views.decorators.cache import cache_control +from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.decorators.http import require_POST +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from django.db import transaction +from common.djangoapps.util.json_request import JsonResponse +from lms.djangoapps.instructor_tools.all_grades_csv import get_all_csv_file + + + +@transaction.non_atomic_requests +@require_POST +@ensure_csrf_cookie +@cache_control(no_cache=True, no_store=True, must_revalidate=True) +def calculate_all_grades_csv(request): + return JsonResponse({'1': 1}) \ No newline at end of file diff --git a/lms/djangoapps/instructor_tools/urls.py b/lms/djangoapps/instructor_tools/urls.py new file mode 100644 index 000000000000..cfcac36f823d --- /dev/null +++ b/lms/djangoapps/instructor_tools/urls.py @@ -0,0 +1,10 @@ +""" +Instructor Tools API endpoint urls. +""" + +from django.urls import path +from lms.djangoapps.instructor_tools import api + +urlpatterns = [ + path('instructor_tools/api/calculate_all_grades_csv', api.calculate_all_grades_csv, name='calculate_all_grades_csv'), +] \ No newline at end of file diff --git a/lms/static/js/instructor_tools/instructor_tools_dashboard.js b/lms/static/js/instructor_tools/instructor_tools_dashboard.js new file mode 100644 index 000000000000..464f26a76c48 --- /dev/null +++ b/lms/static/js/instructor_tools/instructor_tools_dashboard.js @@ -0,0 +1,15 @@ +(function() { + 'use strict'; + // eslint-disable-next-line no-unused-vars + + const GradeAPI = '/instructor_tools/api/calculate_all_grades_csv' + + $(document).ready(function() { + const $gradeReportDownload = $('.grade-report-download'); + $gradeReportDownload.click(function(e) { + $.post(GradeAPI, (res) => { + console.log(res) + }) + }) + }); +}).call(this); \ No newline at end of file diff --git a/lms/templates/instructor_tools/instructor_tools_dashboard.html b/lms/templates/instructor_tools/instructor_tools_dashboard.html index e38f23f9109a..2929e4bc6b2b 100644 --- a/lms/templates/instructor_tools/instructor_tools_dashboard.html +++ b/lms/templates/instructor_tools/instructor_tools_dashboard.html @@ -7,7 +7,11 @@ %> <%block name="headextra"> - +<%static:css group='style-course-vendor'/> +<%static:css group='style-vendor-tinymce-content'/> +<%static:css group='style-vendor-tinymce-skin'/> +<%static:css group='style-course'/> +
diff --git a/lms/urls.py b/lms/urls.py index 53f64814561c..4d0ba90c75b0 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -51,6 +51,7 @@ from openedx.core.djangoapps.user_authn.views.login import redirect_to_lms_login from openedx.features.enterprise_support.api import enterprise_enabled from lms.djangoapps.instructor_tools import views as instructor_tools_views +from lms.djangoapps.instructor_tools import urls as instructor_tools_urls RESET_COURSE_DEADLINES_NAME = 'reset_course_deadlines' RENDER_XBLOCK_NAME = 'render_xblock' @@ -1033,3 +1034,4 @@ urlpatterns += [ path('api/mfe_config/v1', include(('lms.djangoapps.mfe_config_api.urls', 'lms.djangoapps.mfe_config_api'), namespace='mfe_config_api')) ] +urlpatterns += instructor_tools_urls.urlpatterns \ No newline at end of file From 28a13475006322e58cffc6c30ab17095e504a3ea Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 12:41:00 +0700 Subject: [PATCH 077/519] [ADD] Add merged csv task + Add animation --- .../instructor_tools/all_grades_CSV.py | 197 +++++++++++++++++- lms/djangoapps/instructor_tools/api.py | 19 +- .../instructor_tools_dashboard.js | 43 +++- .../instructor_tools_dashboard.html | 1 + 4 files changed, 249 insertions(+), 11 deletions(-) diff --git a/lms/djangoapps/instructor_tools/all_grades_CSV.py b/lms/djangoapps/instructor_tools/all_grades_CSV.py index ca59dc250b31..5c4256e493c5 100644 --- a/lms/djangoapps/instructor_tools/all_grades_CSV.py +++ b/lms/djangoapps/instructor_tools/all_grades_CSV.py @@ -1,7 +1,91 @@ import os +import csv +from datetime import datetime +import pandas as pd directory = '/edx/var/edxapp/uploads/' +lms2019_converter = { + 'Progess test (Avg)': 'Progress test (Avg)', + 'Quiz 1:': 'Quiz 1', + 'Quiz 2:': 'Quiz 2', + 'Quiz 3:': 'Quiz 3', + 'Quiz 4:': 'Quiz 4', + 'Quiz 5:': 'Quiz 5', + 'Quiz 6:': 'Quiz 6', + 'Quiz 7:': 'Quiz 7', + 'Quiz 8:': 'Quiz 8', + 'Quiz 9:': 'Quiz 9', + 'Quiz 10:': 'Quiz 10', + 'Quiz 11:': 'Quiz 11', + 'Quiz 12:': 'Quiz 12', + 'Quiz 13:': 'Quiz 13', + 'Quiz 14:': 'Quiz 14', + 'Quiz 15:': 'Quiz 15', + 'Quiz 16:': 'Quiz 16', + 'Quiz 17:': 'Quiz 17', + 'Quiz 18:': 'Quiz 18', + 'Quiz 19:': 'Quiz 19', + 'Quiz 20:': 'Quiz 20', + 'Quiz 21:': 'Quiz 21', + 'Quiz 22:': 'Quiz 22', + 'Quiz 23:': 'Quiz 23', + 'Quiz 24:': 'Quiz 24', + 'Quiz 25:': 'Quiz 25', + 'Quiz 26:': 'Quiz 26', + 'Quiz 27:': 'Quiz 27', + 'Quiz 28:': 'Quiz 28', + 'Quiz 29:': 'Quiz 29', + 'Quiz 30:': 'Quiz 30', + 'Quiz 31:': 'Quiz 31', + 'Quiz 32:': 'Quiz 32', + 'Quiz 33:': 'Quiz 33', + 'Quiz 34:': 'Quiz 34', + 'Quiz 35:': 'Quiz 35', + 'Quiz 36:': 'Quiz 36', + 'Quiz 37:': 'Quiz 37', + 'Quiz 38:': 'Quiz 38', + 'Quiz 39:': 'Quiz 39', + 'Quiz 40:': 'Quiz 40', + 'Progess test': 'PT 1', + 'Progess test 1:': 'PT 1', + 'Progess test 2:': 'PT 2', + 'Progess test 3:': 'PT 3', + 'Progess test 4:': 'PT 4', + 'Progess test 5:': 'PT 5', + 'Progess test 6:': 'PT 6', + 'Progess test 7:': 'PT 7', + 'Progess test 8:': 'PT 8', + 'Progess test 9:': 'PT 9', + 'Progess test 10:': 'PT 10', + 'Progress test': 'PT 1', + 'Progress test 1:': 'PT 1', + 'Progress test 2:': 'PT 2', + 'Progress test 3:': 'PT 3', + 'Progress test 4:': 'PT 4', + 'Progress test 5:': 'PT 5', + 'Progress test 6:': 'PT 6', + 'Progress test 7:': 'PT 7', + 'Progress test 8:': 'PT 8', + 'Progress test 9:': 'PT 9', + 'Progress test 10:': 'PT 10', + 'Lab 1:': 'Lab 1', + 'Lab 2:': 'Lab 2', + 'Lab 3:': 'Lab 3', + 'Lab 4:': 'Lab 4', + 'Lab 5:': 'Lab 5', + 'Lab 6:': 'Lab 6', + 'Lab 7:': 'Lab 7', + 'Lab 8:': 'Lab 8', + 'Lab 9:': 'Lab 9', + 'Lab 10:': 'Lab 10', + 'Project 1:': 'Project 1', + 'Project 2:': 'Project 2', + 'Project 3:': 'Project 3', + 'Project 4:': 'Project 4', + 'Project 5:': 'Project 5', +} + def get_all_csv_file(): all_dir = [x for x in os.walk(directory)] all_dir.pop(0) @@ -26,4 +110,115 @@ def get_all_csv_file(): latest_file_path = os.path.join(root_path, latest_file) res.append(latest_file_path) - return res \ No newline at end of file + return res + +def process_grade_file(files): + lms_version = 'lms2022' + partner = 'FUNiX' + + all_filenames = [] + for fullpath in files: + # get file name from full_path by split + file = fullpath.split('/')[-1] + + org = file[:file.find('_', 0)] + course = file[file.find('_', 0) + 1 : file.find('_grade_report')] + + with open(fullpath, encoding='utf-8') as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + + # Add two more columns: Org and Course at the beginning of the csv file + output_file = os.path.join(directory, 'Done_' + file) + all_filenames.append(output_file) + + output_csv = open(output_file, 'w', newline='') + csv_writer = csv.writer(output_csv) + line = 0 + + for row in csv_reader: + if line == 0: + row.insert(0, 'Course') + row.insert(0, 'Org') + row.insert(0, 'LMS Version') + row.insert(0, 'Partner') + + # Consolidate column title to odoo field-friendly + col_no = 0 + for col in row: + for key in lms2019_converter: + if col.startswith(key): + row[col_no] = lms2019_converter[key] + + col_no += 1 + else: + row.insert(0, course) + row.insert(0, org) + row.insert(0, lms_version) + row.insert(0, partner) + + csv_writer.writerow(row) + line += 1 + output_csv.close() + + return all_filenames + +def merge_all_csv(files): + combined_csv = pd.concat([pd.read_csv(f) for f in files]) + + # Write to a new file with name = 'combined_csv' and timestamp + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + combined_csv_path = os.path.join(directory, 'combined_csv_' + timestamp + '.csv') + combined_csv.to_csv(combined_csv_path, index=False) + + # Delete all the files + for f in files: + os.remove(f) + + return combined_csv_path + +def process_merged(combined_file): + # create output_file with name = 'Done_combined_all.csv' and timestamp + timestamp = datetime.now().strftime('%Y%m%d%H%M%S') + output_file = os.path.join(directory, 'Done_combined_all_' + timestamp + '.csv') + output_csv = open(output_file, 'w', newline='') + csv_writer = csv.writer(output_csv) + + with open(combined_file) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',') + header = [] + line = 0 + for row in csv_reader: + student_id = row[4] + + # Insert new column: quiz_done, quiz_total + if line == 0: + header = row + row.insert(13, 'Quiz Done') + row.insert(14, 'Quiz Total') + else: + row.insert(13, '') + row.insert(14, '') + + quiz_total = 0 + quiz_done = 0 + col_no = 0 + for col in row: + if not (header[col_no].startswith('Quiz (Avg)') or header[col_no].startswith('Quiz Done') or header[col_no].startswith('Quiz Total')) and header[col_no].startswith('Quiz') and col != '': + quiz_total += 1 + if col == '1.0': + quiz_done += 1 + col_no += 1 + + row[13] = quiz_done + row[14] = quiz_total + + if student_id != 'id': + csv_writer.writerow(row) + + line += 1 + output_csv.close() + + # Delete the combined_file + os.remove(combined_file) + + return output_file \ No newline at end of file diff --git a/lms/djangoapps/instructor_tools/api.py b/lms/djangoapps/instructor_tools/api.py index ad84b20b1f69..14f030dc440c 100644 --- a/lms/djangoapps/instructor_tools/api.py +++ b/lms/djangoapps/instructor_tools/api.py @@ -4,13 +4,26 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from django.db import transaction from common.djangoapps.util.json_request import JsonResponse -from lms.djangoapps.instructor_tools.all_grades_csv import get_all_csv_file - +from lms.djangoapps.instructor_task import api +from opaque_keys.edx.keys import CourseKey +from lms.djangoapps.instructor_tools.all_grades_csv import get_all_csv_file, process_grade_file, merge_all_csv, process_merged @transaction.non_atomic_requests @require_POST @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) def calculate_all_grades_csv(request): - return JsonResponse({'1': 1}) \ No newline at end of file + all_course = CourseOverview.get_all_courses(['FUNiX']) + + for course in all_course: + course_key = CourseKey.from_string(str(course)) + api.submit_calculate_grades_csv_not_async(request, course_key) + + all_csv_files = get_all_csv_file() + all_grade_files = process_grade_file(all_csv_files) + combined_file = merge_all_csv(all_grade_files) + out_file = process_merged(combined_file) + + + return JsonResponse({'output': out_file}) \ No newline at end of file diff --git a/lms/static/js/instructor_tools/instructor_tools_dashboard.js b/lms/static/js/instructor_tools/instructor_tools_dashboard.js index 464f26a76c48..d6edce9ead45 100644 --- a/lms/static/js/instructor_tools/instructor_tools_dashboard.js +++ b/lms/static/js/instructor_tools/instructor_tools_dashboard.js @@ -1,15 +1,44 @@ -(function() { - 'use strict'; - // eslint-disable-next-line no-unused-vars +(function () { + 'use strict'; + // eslint-disable-next-line no-unused-vars const GradeAPI = '/instructor_tools/api/calculate_all_grades_csv' - $(document).ready(function() { + $(document).ready(function () { const $gradeReportDownload = $('.grade-report-download'); - $gradeReportDownload.click(function(e) { + $gradeReportDownload.click(function (e) { + // Show loading spinner by Swal + Swal.fire({ + title: 'Generating...', + html: 'Generating all grade csv file!', + allowOutsideClick: false, + didOpen: () => { + Swal.showLoading() + }, + }); + $.post(GradeAPI, (res) => { - console.log(res) + // Get output and redirect to download + const { + output + } = res; + if (output) { + // Get file name from output path + const fileName = output.split('/').pop(); + + // Go to link in new tab + window.open('http://localhost:18000/media/' + fileName, '_blank'); + } else { + Swal.fire({ + title: 'Error', + text: 'Something went wrong!', + icon: 'error', + }); + } + + // Close the Swal + Swal.close(); }) }) - }); + }); }).call(this); \ No newline at end of file diff --git a/lms/templates/instructor_tools/instructor_tools_dashboard.html b/lms/templates/instructor_tools/instructor_tools_dashboard.html index 2929e4bc6b2b..c0ad4ceffa97 100644 --- a/lms/templates/instructor_tools/instructor_tools_dashboard.html +++ b/lms/templates/instructor_tools/instructor_tools_dashboard.html @@ -12,6 +12,7 @@ <%static:css group='style-vendor-tinymce-skin'/> <%static:css group='style-course'/> +
From 648fa784a9381104246c618142474b50deaf56cf Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 12:48:50 +0700 Subject: [PATCH 078/519] [FIX] bug dates not showing + slow Dashbodard load --- cms/envs/devstack.py | 9 +++---- lms/envs/devstack.py | 11 +++++---- .../funix_relative_date.py | 7 ++++-- .../features/funix_relative_date/models.py | 24 ++++++++++++++----- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 673ab9e08a0e..2cf5fe1fe036 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -111,10 +111,11 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing-function-docstring # We always want the toolbar on devstack unless running tests from another Docker container - hostname = request.get_host() - if hostname.startswith('edx.devstack.studio:') or hostname.startswith('studio.devstack.edx:'): - return False - return True + # hostname = request.get_host() + # if hostname.startswith('edx.devstack.studio:') or hostname.startswith('studio.devstack.edx:'): + # return False + # return True + return False ################################ MILESTONES ################################ diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index a695a297d7b4..b056f58a79dc 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -108,11 +108,12 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing-function-docstring - # We always want the toolbar on devstack unless running tests from another Docker container - hostname = request.get_host() - if hostname.startswith('edx.devstack.lms:') or hostname.startswith('lms.devstack.edx:'): - return False - return True + # # We always want the toolbar on devstack unless running tests from another Docker container + # hostname = request.get_host() + # if hostname.startswith('edx.devstack.lms:') or hostname.startswith('lms.devstack.edx:'): + # return False + # return True + return False ########################### PIPELINE ################################# diff --git a/openedx/features/funix_relative_date/funix_relative_date.py b/openedx/features/funix_relative_date/funix_relative_date.py index b02e8f25a77a..f05162f52d66 100644 --- a/openedx/features/funix_relative_date/funix_relative_date.py +++ b/openedx/features/funix_relative_date/funix_relative_date.py @@ -23,7 +23,10 @@ def get_course_date_blocks(self, course, user, request=None): date_blocks = FunixRelativeDateDAO.get_all_block_by_id(user_id=user.id, course_id=course.id) date_blocks = list(date_blocks) date_blocks.sort(key=lambda x: x.index) - print('---------------date_block:', date_blocks) + + # Check if date_blocks is empty + if len(date_blocks) == 0: + return [] # Add start date start_date = date_blocks.pop(0) output = [ @@ -76,7 +79,7 @@ def get_time(last_complete_date, goal, day=1): if assignment_blocks is None: assignment_blocks = courseware_courses.funix_get_assginment_date_blocks(course=course, user=user, request=None, include_past_dates=True) - last_complete_date = FunixRelativeDateDAO.get_enroll_by_id(user_id=user.id, course_id=course_id).date + last_complete_date = FunixRelativeDateDAO.get_enroll_date_by_id(user_id=user.id, course_id=course_id) if last_complete_date is None: last_complete_date = date.today() diff --git a/openedx/features/funix_relative_date/models.py b/openedx/features/funix_relative_date/models.py index 014c95b2962b..bf0a35274c2f 100644 --- a/openedx/features/funix_relative_date/models.py +++ b/openedx/features/funix_relative_date/models.py @@ -1,6 +1,8 @@ from django.db import models from django.db.models import Q from django.utils.timezone import now +from common.djangoapps.student.models import get_user_by_id +from common.djangoapps.student.models import CourseEnrollment class FunixRelativeDate(models.Model): user_id = models.CharField(max_length=255) @@ -28,18 +30,28 @@ def delete_all_date(self, user_id, course_id): @classmethod def get_all_block_by_id(self, user_id, course_id): - print('-----------get_all_block_by_id:',FunixRelativeDate.objects.filter( - user_id=user_id, - course_id=course_id - ) ) + return FunixRelativeDate.objects.filter( user_id=user_id, course_id=course_id ) @classmethod - def get_enroll_by_id(self, user_id, course_id): - return FunixRelativeDate.objects.filter(user_id=user_id, course_id=course_id, type="start")[0] + def get_enroll_date_by_id(self, user_id, course_id): + try: + enrollment = FunixRelativeDate.objects.filter(user_id=user_id, course_id=course_id, type="start")[0] + + return enrollment.date + except: + try: + user = get_user_by_id(user_id) + enrollment = CourseEnrollment.get_enrollment(user, course_id) + + return enrollment.created + except: + return None + return None + @classmethod def get_all_enroll_by_course(self, course_id): From c0fd7a3d1bd288c583cd1611622f9551e26f0fe2 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:08:44 +0700 Subject: [PATCH 079/519] [CHANGE] Disable DISCUSSION + [FIX] Fix help config --- cms/envs/devstack.py | 2 +- lms/envs/common.py | 2 +- lms/envs/devstack.py | 5 +++++ lms/envs/help_tokens.ini | 25 +++++++++++-------------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/cms/envs/devstack.py b/cms/envs/devstack.py index 2cf5fe1fe036..9e80078b1f29 100644 --- a/cms/envs/devstack.py +++ b/cms/envs/devstack.py @@ -152,7 +152,7 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing ################################ COURSE DISCUSSIONS ########################### -FEATURES['ENABLE_DISCUSSION_SERVICE'] = True +FEATURES['ENABLE_DISCUSSION_SERVICE'] = False ################################ CREDENTIALS ########################### CREDENTIALS_SERVICE_USERNAME = 'credentials_worker' diff --git a/lms/envs/common.py b/lms/envs/common.py index 9ff0be13cdd7..0aa0e4e892ec 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -138,7 +138,7 @@ # attempting to expand those components will cause errors. So, this should only be set to False with an LMS that # is running courses that do not contain discussion components. # For consistency in user-experience, keep the value in sync with the setting of the same name in the CMS. - 'ENABLE_DISCUSSION_SERVICE': True, + 'ENABLE_DISCUSSION_SERVICE': False, # .. toggle_name: FEATURES['ENABLE_TEXTBOOK'] # .. toggle_implementation: DjangoSetting diff --git a/lms/envs/devstack.py b/lms/envs/devstack.py index b056f58a79dc..4458ac1791b4 100644 --- a/lms/envs/devstack.py +++ b/lms/envs/devstack.py @@ -396,6 +396,11 @@ def should_show_debug_toolbar(request): # lint-amnesty, pylint: disable=missing 'ENABLE_ENTERPRISE_INTEGRATION': True, }) +HELP_TOKENS_BOOKS = { + 'course_author': 'https://funix.gitbook.io/funix-documentation/', + 'learner': 'https://funix.gitbook.io/funix-documentation/', +} + ENABLE_MKTG_SITE = os.environ.get('ENABLE_MARKETING_SITE', False) MARKETING_SITE_ROOT = os.environ.get('MARKETING_SITE_ROOT', 'http://localhost:8080') diff --git a/lms/envs/help_tokens.ini b/lms/envs/help_tokens.ini index 55ee7702fb05..dc08fe442742 100644 --- a/lms/envs/help_tokens.ini +++ b/lms/envs/help_tokens.ini @@ -2,21 +2,18 @@ # NOTE: If any of these page settings change, then their corresponding test should be updated # in edx-platform/common/test/acceptance/tests/lms/test_lms_help.py [pages] -default = learner:index.html -instructor = course_author:CA_instructor_dash_help.html -course = learner:index.html -profile = learner:SFD_dashboard_profile_SectionHead.html#adding-profile-information -dashboard = learner:SFD_dashboard_profile_SectionHead.html -courseinfo = learner:SFD_start_course.html -progress = learner:SFD_check_progress.html -learneraccountsettings = learner:SFD_update_acct_settings.html -learnerdashboard = learner:SFD_dashboard_profile_SectionHead.html -programs = learner:SFD_enrolling.html -bookmarks = learner:SFD_bookmarks.html -notes = learner:SFD_notes.html -wiki = learner:SFD_wiki.html -discussions = learner:sfd_discussions/index.html +profile = learner:index.html#adding-profile-information +dashboard = learner:index.html +courseinfo = learner:index.html +progress = learner:index.html +learneraccountsettings = learner:index.html +learnerdashboard = learner:index.html +programs = learner:index.html +bookmarks = learner:index.html +notes = learner:index.html +wiki = learner:index.html +discussions = learner:index.html cohortmanual = course_author:course_features/cohorts/cohort_config.html#assign-learners-to-cohorts-manually cohortautomatic = course_author:course_features/cohorts/cohorts_overview.html#all-automated-assignment From 9ddd207e26cae9c1bf7f1b206b3d0da03de4796a Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 13:14:53 +0700 Subject: [PATCH 080/519] [ADD] ADD_HF --- cms/static/images/favicon.ico | Bin 0 -> 16958 bytes cms/static/images/logo-large.png | Bin 0 -> 14391 bytes cms/static/images/logo.png | Bin 0 -> 1091 bytes cms/templates/widgets/footer.html | 4 ++-- lms/djangoapps/branding/api.py | 22 ++++++++++++---------- lms/static/images/favicon.ico | Bin 30237 -> 16958 bytes lms/static/images/logo-large.png | Bin 7634 -> 14391 bytes lms/static/images/logo.png | Bin 570 -> 1091 bytes lms/templates/footer.html | 17 +++++++++++++---- 9 files changed, 27 insertions(+), 16 deletions(-) create mode 100644 cms/static/images/favicon.ico create mode 100644 cms/static/images/logo-large.png create mode 100644 cms/static/images/logo.png diff --git a/cms/static/images/favicon.ico b/cms/static/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..d6442392e271b5ae7e0842d62255c29f0e7c6520 GIT binary patch literal 16958 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 literal 0 HcmV?d00001 diff --git a/cms/static/images/logo.png b/cms/static/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..94db2e8a2158539a4ff87ec6ec27b75ec5c434e7 GIT binary patch literal 1091 zcmV-J1ibr+P)7{O|OO4<8APu=4d!7}=-!@4b-jo^VC<7KTquE5b)W zd?BQ}BRqhQ2Zrz}@q@NJzITO=p41TT;e6880s2iI{unKNO#@5p{%zu&;h8IL@=hO5JU!%c&l^-~x#{JYf4DsrWhmc~-Qudc5! z<~$M3KBytYZ*UHUC1L*__t_f=516=-_jIyhDB~V+v&e$*OT*mJIy)paK2d&_1mTDE zdYX4{Ej90w5rHj3!~Di_9d$cBAYOfD#QcQY2fkM~k(x`@Ev$O_n$fQ=!sP7>%QaH% z?;YdKlr&TO=RkON)O37foOi5X{JybaE~(kCDd%pq-zDZ(m<;khHp%5GRM zCSpyXHnO6pjNh4D^#&G8WT~V{^(R7fblr{|^M7D$)hAY}EDYI9ndVY^y?DOIMBu{fCgKxPJ0IhjRmNtJnUIl{!nO_Y ztBZ`IROp0^OU-o%E}PM1W!9t(pn^lv(BMmHo~2l^`dSJ(a`l%9^bL_O)t4;s6s~# z$EA7uhDagVf~g_lzQxTMaxa++q)(_>Z-KyZP#tO>$QDcuux3hqpLoeDy_#=BUWzk` z3>77gI-PDomw$O?tRyBs69A%9z`E zkeSwsBWw-do)H9<7u~dQrcuK)C5#kJYi{6_E1FuZNU=Z}-~nv%{vGP?z?FwHx6tZL z7-y(Bv_adNrmF{;dG*d9_aAWHuox2v1OkCTAP@)y0`I>70{}h7r;vwb3c>&Y002ov JPDHLkV1gI=`W65H literal 0 HcmV?d00001 diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index b6c7d2f45eb0..d15387c9ec42 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -20,7 +20,7 @@

@@ -148,16 +148,25 @@ ## through an API to partner sites (such as marketing sites or blogs), ## which are not technically powered by OpenEdX. % if not hide_openedx_link: - {% endcomment %} % endif + + + + + % endif % if include_dependencies: <%static:js group='base_vendor'/> From 7ac13cc55dd54f446a6b705db52b1b6af9ad6635 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 14:33:40 +0700 Subject: [PATCH 081/519] Rename all_grades_CSV.py to all_grades_csv.py --- .../instructor_tools/{all_grades_CSV.py => all_grades_csv.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename lms/djangoapps/instructor_tools/{all_grades_CSV.py => all_grades_csv.py} (96%) diff --git a/lms/djangoapps/instructor_tools/all_grades_CSV.py b/lms/djangoapps/instructor_tools/all_grades_csv.py similarity index 96% rename from lms/djangoapps/instructor_tools/all_grades_CSV.py rename to lms/djangoapps/instructor_tools/all_grades_csv.py index 5c4256e493c5..41b4e08d3e49 100644 --- a/lms/djangoapps/instructor_tools/all_grades_CSV.py +++ b/lms/djangoapps/instructor_tools/all_grades_csv.py @@ -221,4 +221,4 @@ def process_merged(combined_file): # Delete the combined_file os.remove(combined_file) - return output_file \ No newline at end of file + return output_file From bbbb9390f990069276f37e669f4a3be4033f10ed Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 15:19:21 +0700 Subject: [PATCH 082/519] a --- lms/djangoapps/instructor_tools/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor_tools/api.py b/lms/djangoapps/instructor_tools/api.py index 14f030dc440c..622972016cf6 100644 --- a/lms/djangoapps/instructor_tools/api.py +++ b/lms/djangoapps/instructor_tools/api.py @@ -7,7 +7,7 @@ from lms.djangoapps.instructor_task import api from opaque_keys.edx.keys import CourseKey -from lms.djangoapps.instructor_tools.all_grades_csv import get_all_csv_file, process_grade_file, merge_all_csv, process_merged +from lms.djangoapps.instructor_tools.all_grades_CSV import get_all_csv_file, process_grade_file, merge_all_csv, process_merged @transaction.non_atomic_requests @require_POST From 328512f62eaab647d0901ea3908d1cb2cb378ff4 Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Fri, 9 Jun 2023 16:12:35 +0700 Subject: [PATCH 083/519] a --- lms/djangoapps/instructor_tools/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/djangoapps/instructor_tools/api.py b/lms/djangoapps/instructor_tools/api.py index 622972016cf6..14f030dc440c 100644 --- a/lms/djangoapps/instructor_tools/api.py +++ b/lms/djangoapps/instructor_tools/api.py @@ -7,7 +7,7 @@ from lms.djangoapps.instructor_task import api from opaque_keys.edx.keys import CourseKey -from lms.djangoapps.instructor_tools.all_grades_CSV import get_all_csv_file, process_grade_file, merge_all_csv, process_merged +from lms.djangoapps.instructor_tools.all_grades_csv import get_all_csv_file, process_grade_file, merge_all_csv, process_merged @transaction.non_atomic_requests @require_POST From b02304197772267d383b955b8db759bbcebff69a Mon Sep 17 00:00:00 2001 From: sonxauxi2411 <112046655+sonxauxi2411@users.noreply.github.com> Date: Sat, 10 Jun 2023 11:17:47 +0700 Subject: [PATCH 084/519] fix footer --- cms/templates/widgets/footer.html | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/cms/templates/widgets/footer.html b/cms/templates/widgets/footer.html index d15387c9ec42..0bc71e70e380 100644 --- a/cms/templates/widgets/footer.html +++ b/cms/templates/widgets/footer.html @@ -20,16 +20,7 @@