Skip to content

Commit

Permalink
Merge branch 'master' into submit_button_reset
Browse files Browse the repository at this point in the history
  • Loading branch information
e0d authored Aug 7, 2024
2 parents a7d9c3a + 99760f8 commit ee0010d
Show file tree
Hide file tree
Showing 30 changed files with 815 additions and 413 deletions.
2 changes: 1 addition & 1 deletion cms/djangoapps/contentstore/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2255,7 +2255,7 @@ def send_course_update_notification(course_key, content, user):
"course_update_content": text_content if len(text_content.strip()) < 10 else "Click here to view",
**extra_context,
},
notification_type="course_update",
notification_type="course_updates",
content_url=f"{settings.LMS_ROOT_URL}/courses/{str(course_key)}/course/updates",
app_name="updates",
audience_filters={},
Expand Down
3 changes: 2 additions & 1 deletion lms/djangoapps/grades/grade_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from datetime import timedelta

from django.utils import timezone
from django.conf import settings

from openedx.core.djangoapps.content.course_overviews.models import CourseOverview

Expand All @@ -22,7 +23,7 @@ def are_grades_frozen(course_key):
if ENFORCE_FREEZE_GRADE_AFTER_COURSE_END.is_enabled(course_key):
course = CourseOverview.get_from_id(course_key)
if course.end:
freeze_grade_date = course.end + timedelta(30)
freeze_grade_date = course.end + timedelta(settings.GRADEBOOK_FREEZE_DAYS)
now = timezone.now()
return now > freeze_grade_date
return False
138 changes: 74 additions & 64 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@
from lms.djangoapps.instructor_task.api_helper import AlreadyRunningError, QueueConnectionError
from lms.djangoapps.instructor_task.data import InstructorTaskTypes
from lms.djangoapps.instructor_task.models import ReportStore
from lms.djangoapps.instructor.views.serializer import RoleNameSerializer, UserSerializer
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.course_groups.cohorts import add_user_to_cohort, is_course_cohorted
from openedx.core.djangoapps.course_groups.models import CourseUserGroup
Expand Down Expand Up @@ -1064,15 +1065,11 @@ def modify_access(request, course_id):
return JsonResponse(response_payload)


@require_POST
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_course_permission(permissions.EDIT_COURSE_ACCESS)
@require_post_params(rolename="'instructor', 'staff', or 'beta'")
def list_course_role_members(request, course_id):
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
class ListCourseRoleMembersView(APIView):
"""
List instructors and staff.
Requires instructor access.
View to list instructors and staff for a specific course.
Requires the user to have instructor access.
rolename is one of ['instructor', 'staff', 'beta', 'ccx_coach']
Expand All @@ -1088,33 +1085,41 @@ def list_course_role_members(request, course_id):
]
}
"""
course_id = CourseKey.from_string(course_id)
course = get_course_with_access(
request.user, 'instructor', course_id, depth=None
)
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.EDIT_COURSE_ACCESS

rolename = request.POST.get('rolename')
@method_decorator(ensure_csrf_cookie)
def post(self, request, course_id):
"""
Handles POST request to list instructors and staff.
if rolename not in ROLES:
return HttpResponseBadRequest()
Args:
request (HttpRequest): The request object containing user data.
course_id (str): The ID of the course to list instructors and staff for.
def extract_user_info(user):
""" convert user into dicts for json view """
Returns:
Response: A Response object containing the list of instructors and staff or an error message.
return {
'username': user.username,
'email': user.email,
'first_name': user.first_name,
'last_name': user.last_name,
Raises:
Http404: If the course does not exist.
"""
course_id = CourseKey.from_string(course_id)
course = get_course_with_access(
request.user, 'instructor', course_id, depth=None
)
role_serializer = RoleNameSerializer(data=request.data)
role_serializer.is_valid(raise_exception=True)
rolename = role_serializer.data['rolename']

users = list_with_level(course.id, rolename)
serializer = UserSerializer(users, many=True)

response_payload = {
'course_id': str(course_id),
rolename: serializer.data,
}

response_payload = {
'course_id': str(course_id),
rolename: list(map(extract_user_info, list_with_level(
course.id, rolename
))),
}
return JsonResponse(response_payload)
return Response(response_payload, status=status.HTTP_200_OK)


class ProblemResponseReportPostParamsSerializer(serializers.Serializer): # pylint: disable=abstract-method
Expand Down Expand Up @@ -2366,46 +2371,51 @@ def _list_instructor_tasks(request, course_id):
return JsonResponse(response_payload)


@require_POST
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_course_permission(permissions.SHOW_TASKS)
def list_entrance_exam_instructor_tasks(request, course_id):
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
class ListEntranceExamInstructorTasks(APIView):
"""
List entrance exam related instructor tasks.
Takes either of the following query parameters
- unique_student_identifier is an email or username
- all_students is a boolean
"""
course_id = CourseKey.from_string(course_id)
course = get_course_by_id(course_id)
student = request.POST.get('unique_student_identifier', None)
if student is not None:
student = get_student_from_identifier(student)
permission_classes = (IsAuthenticated, permissions.InstructorPermission)
permission_name = permissions.SHOW_TASKS

try:
entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id)
except InvalidKeyError:
return HttpResponseBadRequest(_("Course has no valid entrance exam section."))
if student:
# Specifying for a single student's entrance exam history
tasks = task_api.get_entrance_exam_instructor_task_history(
course_id,
entrance_exam_key,
student
)
else:
# Specifying for all student's entrance exam history
tasks = task_api.get_entrance_exam_instructor_task_history(
course_id,
entrance_exam_key
)
@method_decorator(ensure_csrf_cookie)
def post(self, request, course_id):
"""
List entrance exam related instructor tasks.
response_payload = {
'tasks': list(map(extract_task_features, tasks)),
}
return JsonResponse(response_payload)
Takes either of the following query parameters
- unique_student_identifier is an email or username
- all_students is a boolean
"""
course_id = CourseKey.from_string(course_id)
course = get_course_by_id(course_id)
student = request.POST.get('unique_student_identifier', None)
if student is not None:
student = get_student_from_identifier(student)

try:
entrance_exam_key = UsageKey.from_string(course.entrance_exam_id).map_into_course(course_id)
except InvalidKeyError:
return HttpResponseBadRequest(_("Course has no valid entrance exam section."))
if student:
# Specifying for a single student's entrance exam history
tasks = task_api.get_entrance_exam_instructor_task_history(
course_id,
entrance_exam_key,
student
)
else:
# Specifying for all student's entrance exam history
tasks = task_api.get_entrance_exam_instructor_task_history(
course_id,
entrance_exam_key
)

response_payload = {
'tasks': list(map(extract_task_features, tasks)),
}
return JsonResponse(response_payload)


class ReportDownloadSerializer(serializers.Serializer): # pylint: disable=abstract-method
Expand Down
4 changes: 2 additions & 2 deletions lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
urlpatterns = [
path('students_update_enrollment', api.students_update_enrollment, name='students_update_enrollment'),
path('register_and_enroll_students', api.register_and_enroll_students, name='register_and_enroll_students'),
path('list_course_role_members', api.list_course_role_members, name='list_course_role_members'),
path('list_course_role_members', api.ListCourseRoleMembersView.as_view(), name='list_course_role_members'),
path('modify_access', api.modify_access, name='modify_access'),
path('bulk_beta_modify_access', api.bulk_beta_modify_access, name='bulk_beta_modify_access'),
path('get_problem_responses', api.get_problem_responses, name='get_problem_responses'),
Expand All @@ -40,7 +40,7 @@
path('reset_student_attempts_for_entrance_exam', api.reset_student_attempts_for_entrance_exam,
name='reset_student_attempts_for_entrance_exam'),
path('rescore_entrance_exam', api.rescore_entrance_exam, name='rescore_entrance_exam'),
path('list_entrance_exam_instructor_tasks', api.list_entrance_exam_instructor_tasks,
path('list_entrance_exam_instructor_tasks', api.ListEntranceExamInstructorTasks.as_view(),
name='list_entrance_exam_instructor_tasks'),
path('mark_student_can_skip_entrance_exam', api.mark_student_can_skip_entrance_exam,
name='mark_student_can_skip_entrance_exam'),
Expand Down
30 changes: 30 additions & 0 deletions lms/djangoapps/instructor/views/serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
""" Instructor apis serializers. """

from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.core.exceptions import ValidationError
from django.utils.translation import gettext as _
from rest_framework import serializers

from lms.djangoapps.instructor.access import ROLES


class RoleNameSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Serializer that describes the response of the problem response report generation API.
"""

rolename = serializers.CharField(help_text=_("Role name"))

def validate_rolename(self, value):
"""
Check that the rolename is valid.
"""
if value not in ROLES:
raise ValidationError(_("Invalid role name."))
return value


class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['username', 'email', 'first_name', 'last_name']
5 changes: 5 additions & 0 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -1101,6 +1101,11 @@
# If this is true, random scores will be generated for the purpose of debugging the profile graphs
GENERATE_PROFILE_SCORES = False

# .. setting_name: GRADEBOOK_FREEZE_DAYS
# .. setting_default: 30
# .. setting_description: Sets the number of days after which the gradebook will freeze following the course's end.
GRADEBOOK_FREEZE_DAYS = 30

# Used with XQueue
XQUEUE_WAITTIME_BETWEEN_REQUESTS = 5 # seconds
XQUEUE_INTERFACE = {
Expand Down
8 changes: 4 additions & 4 deletions openedx/core/djangoapps/notifications/base_notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,13 @@
'email_template': '',
'filters': [FILTER_AUDIT_EXPIRED_USERS_WITH_NO_ROLE]
},
'course_update': {
'course_updates': {
'notification_app': 'updates',
'name': 'course_update',
'name': 'course_updates',
'is_core': False,
'info': '',
'web': True,
'email': True,
'email': False,
'push': True,
'email_cadence': EmailCadence.DAILY,
'non_editable': [],
Expand All @@ -197,7 +197,7 @@
'push': False,
'email_cadence': EmailCadence.DAILY,
'non_editable': [],
'content_template': _('<{p}>You have a new open response submission awaiting for review for : '
'content_template': _('<{p}>You have a new open response submission awaiting for review for '
'<{strong}>{ora_name}</{strong}></{p}>'),
'content_context': {
'ora_name': 'Name of ORA in course',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def test_create_app_notifications_dict(self):
"""
Notification.objects.all().delete()
create_notification(self.user, self.course.id, app_name='discussion', notification_type='new_comment')
create_notification(self.user, self.course.id, app_name='updates', notification_type='course_update')
create_notification(self.user, self.course.id, app_name='updates', notification_type='course_updates')
app_dict = create_app_notifications_dict(Notification.objects.all())
assert len(app_dict.keys()) == 2
for key in ['discussion', 'updates']:
Expand Down Expand Up @@ -130,7 +130,7 @@ def test_email_digest_context(self, digest_frequency):
discussion_notification = create_notification(self.user, self.course.id, app_name='discussion',
notification_type='new_comment')
update_notification = create_notification(self.user, self.course.id, app_name='updates',
notification_type='course_update')
notification_type='course_updates')
app_dict = create_app_notifications_dict(Notification.objects.all())
end_date = datetime.datetime(2024, 3, 24, 12, 0)
params = {
Expand Down
2 changes: 1 addition & 1 deletion openedx/core/djangoapps/notifications/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
ADDITIONAL_NOTIFICATION_CHANNEL_SETTINGS = ['email_cadence']

# Update this version when there is a change to any course specific notification type or app.
COURSE_NOTIFICATION_CONFIG_VERSION = 10
COURSE_NOTIFICATION_CONFIG_VERSION = 11


def get_course_notification_preference_config():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ <h3 style="font-size: 1.375rem; font-weight:700; line-height:28px; margin: 0.75r
</td>
<td class="notification-content" width="100%" align="left" valign="top" style=" padding: 1rem 1rem 1rem 0.5rem">
<p style="font-size: 0.875rem; font-weight:400; line-height:24px; color:#454545; margin: 0;">
{{ notification.content | safe }}
{{ notification.content | truncatechars_html:600 | safe }}
</p>
<p style="height: 0.5rem; margin: 0"></p>
<p style="color:#707070; margin: 0">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{ _("Email Digest Preferences Updated") }}</title>
</head>
<body>
<script>
alert('{{ _("You have successfully unsubscribed from email digest for learning activity") }}');
window.location.replace("{{ notification_preferences_url }}");
</script>
</body>
</html>
2 changes: 1 addition & 1 deletion openedx/core/djangoapps/notifications/tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def test_app_name_param(self):
"""
assert not Notification.objects.all()
create_notification(self.user, self.course_1.id, app_name='discussion', notification_type='new_comment')
create_notification(self.user, self.course_1.id, app_name='updates', notification_type='course_update')
create_notification(self.user, self.course_1.id, app_name='updates', notification_type='course_updates')
delete_notifications({'app_name': 'discussion'})
assert not Notification.objects.filter(app_name='discussion')
assert Notification.objects.filter(app_name='updates')
Expand Down
4 changes: 2 additions & 2 deletions openedx/core/djangoapps/notifications/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,9 @@ def _expected_api_response(self, course=None):
'enabled': True,
'core_notification_types': [],
'notification_types': {
'course_update': {
'course_updates': {
'web': True,
'email': True,
'email': False,
'push': True,
'email_cadence': 'Daily',
'info': ''
Expand Down
8 changes: 5 additions & 3 deletions openedx/core/djangoapps/notifications/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

from django.conf import settings
from django.db.models import Count
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.shortcuts import get_object_or_404, render
from django.utils.translation import gettext as _
from opaque_keys.edx.keys import CourseKey
from pytz import UTC
Expand Down Expand Up @@ -442,4 +441,7 @@ def preference_update_from_encrypted_username_view(request, username, patch):
username and patch must be string
"""
update_user_preferences_from_patch(username, patch)
return HttpResponse("<!DOCTYPE html><html><body>Success</body></html>", status=status.HTTP_200_OK)
context = {
"notification_preferences_url": f"{settings.ACCOUNT_MICROFRONTEND_URL}/notifications"
}
return render(request, "notifications/email_digest_preference_update.html", context=context)
2 changes: 1 addition & 1 deletion openedx/core/djangoapps/xblock/rest_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ def post(self, request, usage_key_str):

# Signal that we've modified this block
context_impl = get_learning_context_impl(usage_key)
context_impl.send_updated_event(usage_key)
context_impl.send_block_updated_event(usage_key)

return Response({
"id": str(block.location),
Expand Down
Loading

0 comments on commit ee0010d

Please sign in to comment.