- % endif
% endif
% endif
diff --git a/common/templates/xblock_v2/xblock_iframe.html b/common/templates/xblock_v2/xblock_iframe.html
index 39eb044b183e..cd3096aa4626 100644
--- a/common/templates/xblock_v2/xblock_iframe.html
+++ b/common/templates/xblock_v2/xblock_iframe.html
@@ -40,6 +40,7 @@
// The minimal RequireJS configuration required for common LMS and CMS building XBlock types to work:
require = require || RequireJS.require;
define = define || RequireJS.define;
+ baseUrl = "";
(function (require, define) {
if ('{{ view_name | safe }}' === 'studio_view') {
// Call `require-config.js` of the CMS
@@ -47,9 +48,10 @@
script.type = 'text/javascript';
script.src = "{{ cms_root_url }}/static/studio/cms/js/require-config.js";
document.head.appendChild(script);
+ baseUrl = "{{ cms_root_url }}/static/studio";
require.config({
- baseUrl: "{{ cms_root_url }}/static/studio",
+ baseUrl,
paths: {
accessibility: 'js/src/accessibility_tools',
draggabilly: 'js/vendor/draggabilly',
@@ -64,8 +66,9 @@
"{{ lms_root_url }}/static/dist{{ oa_manifest.oa_editor_textarea_js }}",
]);
} else {
+ baseUrl = "{{ lms_root_url }}/static/";
require.config({
- baseUrl: "{{ lms_root_url }}/static/",
+ baseUrl,
paths: {
accessibility: 'js/src/accessibility_tools',
draggabilly: 'js/vendor/draggabilly',
@@ -273,8 +276,6 @@
// Check if the XBlock has an initialization function:
const initFunctionName = element.getAttribute('data-init');
if (initFunctionName !== null) {
- // Since this block has an init function, it may need to call handlers:
- element[HANDLER_URL] = HANDLER_URL_MAP[usageId];
// Now proceed with initializing the block's JavaScript:
const InitFunction = (window)[initFunctionName];
// Does the XBlock HTML contain arguments to pass to the InitFunction?
@@ -293,13 +294,12 @@
// to pass 'element' as a jQuery-wrapped DOM element, whereas the LMS
// runtime used to pass 'element' as the pure DOM node. In order not to
// break backwards compatibility, we would need to maintain that.
- // However, this is currently disabled as it causes issues (need to
- // modify the runtime methods like handlerUrl too), and we decided not
- // to maintain support for legacy studio_view in this runtime.
- // const isStudioView = element.className.indexOf('studio_view') !== -1;
- // const passElement = isStudioView && (window as any).$ ? (window as any).$(element) : element;
- const blockJS = new InitFunction(runtime, element, data) || {};
- blockJS.element = element;
+ const isStudioView = element.className.indexOf('studio_view') !== -1;
+ const passElement = isStudioView && window.$ ? window.$(element) : element;
+ // Since this block has an init function, it may need to call handlers:
+ passElement[HANDLER_URL] = HANDLER_URL_MAP[usageId];
+ const blockJS = new InitFunction(runtime, passElement, data) || {};
+ blockJS.element = passElement;
if (['MetadataOnlyEditingDescriptor', 'SequenceDescriptor'].includes(data['xmodule-type'])) {
// The xmodule type `MetadataOnlyEditingDescriptor` and `SequenceDescriptor` renders a `
` with
@@ -308,7 +308,7 @@
// editor using the metadata.
require(['{{ cms_root_url }}/static/studio/js/views/xblock_editor.js'], function(XBlockEditorView) {
var editorView = new XBlockEditorView({
- el: element,
+ el: passElement,
xblock: blockJS,
});
// To render block using metadata
@@ -327,35 +327,28 @@
`;
- element.innerHTML += xblockActions;
-
- const views = editorView.getMetadataEditor().views;
- Object.values(views).forEach(view => {
- const uniqueId = view.uniqueId;
- const input = element.querySelector(`#${uniqueId}`);
- if (input) {
- input.addEventListener("input", function(event) {
- view.model.setValue(event.target.value);
- });
- }
- });
-
+ // Check if passElement is a jQuery-wrapped dom element
+ if (passElement.jquery) {
+ passElement.append(xblockActions);
+ } else {
+ passElement.innerHTML += xblockActions;
+ }
// Adding cancel functionality
- $('.cancel-button', element).bind('click', function() {
+ $('.cancel-button', passElement).bind('click', function() {
runtime.notify('cancel', {});
event.preventDefault();
});
// Adding save functionality
- $('.save-button', element).bind('click', function() {
+ $('.save-button', passElement).bind('click', function() {
//event.preventDefault();
- var error_message_div = $('.xblock-editor-error-message', element);
- const modifiedData = editorView.getChangedMetadata();
+ var error_message_div = $('.xblock-editor-error-message', passElement);
+ const modifiedData = editorView.getChangedMetadata(true);
error_message_div.html();
error_message_div.css('display', 'none');
- var handlerUrl = runtime.handlerUrl(element, 'studio_submit');
+ var handlerUrl = runtime.handlerUrl(passElement, 'studio_submit');
runtime.notify('save', {state: 'start', message: gettext("Saving")});
@@ -385,7 +378,6 @@
if ('{{ view_name | safe }}' === 'studio_view') {
// Used when rendering the `studio_view`, in order to avoid open a new tab on click cancel or save
const selectors = [
- '.cancel-button',
'.save-button',
'.action-cancel',
'.action-save',
@@ -401,6 +393,24 @@
});
}
}
+
+ // This button is used in `StudioEditableXBlockMixin` from the `Xblock` app
+ // That app adds a listener that removes any TinyMCE editors on click cancel.
+ // ref: https://github.com/openedx/XBlock/blob/86eee4b05dffa42b009fab2a9050b73766131b9d/xblock/utils/public/studio_edit.js#L169
+ //
+ // Here that is an issue because we show a confirmation modal when clicking cancel,
+ // if the user stays to edit all TinyMCE editors are no longer there.
+ //
+ // We uncouple the listener to avoid remove the TinyMCE editors
+ const extraCancelSelector = '.cancel-button';
+ const elements = $(extraCancelSelector).first();
+ if (elements.length) {
+ elements.first().unbind("click");
+ elements.on('click', function() {
+ event.preventDefault();
+ runtime.notify('cancel', {});
+ });
+ }
}
}
@@ -455,9 +465,6 @@
// it will report the height of its contents to the parent window when the
// document loads, window resizes, or DOM mutates.
if (window !== window.parent) {
- var lastHeight = window.parent[0].offsetHeight;
- var lastWidth = window.parent[0].offsetWidth;
-
function dispatchResizeMessage(event) {
// Note: event is actually an Array of MutationRecord objects when fired from the MutationObserver
var newHeight = rootNode.scrollHeight;
@@ -472,10 +479,6 @@
}
}, document.referrer
);
-
- lastHeight = newHeight;
- lastWidth = newWidth;
-
// Within the authoring microfrontend the iframe resizes to match the
// height of this document and it should never scroll. It does scroll
// ocassionally when javascript is used to focus elements on the page
diff --git a/common/test/data/toy/static/python_lib.zip b/common/test/data/toy/static/python_lib.zip
new file mode 100644
index 000000000000..5854e82b68d9
Binary files /dev/null and b/common/test/data/toy/static/python_lib.zip differ
diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py
index 3b6cc75e4890..1a2651166508 100644
--- a/lms/djangoapps/certificates/views/webview.py
+++ b/lms/djangoapps/certificates/views/webview.py
@@ -121,7 +121,8 @@ def _update_certificate_context(context, course, course_overview, user_certifica
else:
date = display_date_for_certificate(course, user_certificate)
# Translators: The format of the date includes the full name of the month
- context['certificate_date_issued'] = strftime_localized(date, settings.CERTIFICATE_DATE_FORMAT)
+ # context['certificate_date_issued'] = strftime_localized(date, settings.CERTIFICATE_DATE_FORMAT)
+ context['certificate_date_issued'] = date.strftime("%d.%m.%Y")
# Translators: This text represents the verification of the certificate
context['document_meta_description'] = _('This is a valid {platform_name} certificate for {user_name}, '
diff --git a/lms/djangoapps/course_api/serializers.py b/lms/djangoapps/course_api/serializers.py
index 5fc711376784..bd4e981d2e9d 100644
--- a/lms/djangoapps/course_api/serializers.py
+++ b/lms/djangoapps/course_api/serializers.py
@@ -17,7 +17,6 @@
from openedx.core.djangoapps.models.course_details import CourseDetails
from openedx.core.lib.api.fields import AbsoluteURLField
-
class _MediaSerializer(serializers.Serializer): # pylint: disable=abstract-method
"""
Nested serializer to represent a media object.
@@ -100,6 +99,7 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
blocks_url = serializers.SerializerMethodField()
effort = serializers.CharField()
+ # complexity = serializers.SerializerMethodField()
end = serializers.DateTimeField()
enrollment_start = serializers.DateTimeField()
enrollment_end = serializers.DateTimeField()
@@ -120,6 +120,9 @@ class CourseSerializer(serializers.Serializer): # pylint: disable=abstract-meth
# 'course_id' is a deprecated field, please use 'id' instead.
course_id = serializers.CharField(source='id', read_only=True)
+ def get_complexity(self, obj):
+ return CourseDetails.fetch_about_attribute(obj.id, 'complexity')
+
def get_hidden(self, course_overview):
"""
Get the representation for SerializerMethodField `hidden`
diff --git a/lms/djangoapps/courseware/views/views.py b/lms/djangoapps/courseware/views/views.py
index 3e7a3fae8ae4..c47ab97ea249 100644
--- a/lms/djangoapps/courseware/views/views.py
+++ b/lms/djangoapps/courseware/views/views.py
@@ -285,37 +285,22 @@ def user_groups(user):
return group_names
+
@ensure_csrf_cookie
@cache_if_anonymous()
def courses(request):
"""
- Render "find courses" page. The course selection work is done in courseware.courses.
+ Render "find courses" page. Only show courses with catalog_visibility = both.
"""
- courses_list = []
- course_discovery_meanings = getattr(settings, 'COURSE_DISCOVERY_MEANINGS', {})
- set_default_filter = ENABLE_COURSE_DISCOVERY_DEFAULT_LANGUAGE_FILTER.is_enabled()
- if not settings.FEATURES.get('ENABLE_COURSE_DISCOVERY'):
- courses_list = get_courses(
- request.user,
- filter_={"catalog_visibility": CATALOG_VISIBILITY_CATALOG_AND_ABOUT},
- )
-
- if configuration_helpers.get_value("ENABLE_COURSE_SORTING_BY_START_DATE",
- settings.FEATURES["ENABLE_COURSE_SORTING_BY_START_DATE"]):
- courses_list = sort_by_start_date(courses_list)
- else:
- courses_list = sort_by_announcement(courses_list)
-
- # Add marketable programs to the context.
- programs_list = get_programs_with_type(request.site, include_hidden=False)
+ courses_list = get_courses(
+ request.user,
+ filter_={"catalog_visibility": CATALOG_VISIBILITY_CATALOG_AND_ABOUT},
+ )
return render_to_response(
- "courseware/courses.html",
+ "courseware/courses_summer.html",
{
'courses': courses_list,
- 'course_discovery_meanings': course_discovery_meanings,
- 'set_default_filter': set_default_filter,
- 'programs_list': programs_list,
}
)
@@ -866,13 +851,17 @@ def course_about(request, course_id): # pylint: disable=too-many-statements
# Overview
overview = CourseOverview.get_from_id(course.id)
-
+ same_name_courses = CourseOverview.objects.filter(
+ display_name=overview.display_name
+ ).order_by('start')
sidebar_html_enabled = ENABLE_COURSE_ABOUT_SIDEBAR_HTML.is_enabled()
allow_anonymous = check_public_access(course, [COURSE_VISIBILITY_PUBLIC, COURSE_VISIBILITY_PUBLIC_OUTLINE])
+ complexity = overview.complexity
context = {
'course': course,
+ 'complexity': complexity,
'course_details': course_details,
'staff_access': staff_access,
'studio_url': studio_url,
@@ -897,6 +886,7 @@ def course_about(request, course_id): # pylint: disable=too-many-statements
'course_image_urls': overview.image_urls,
'sidebar_html_enabled': sidebar_html_enabled,
'allow_anonymous': allow_anonymous,
+ 'same_name_courses': same_name_courses,
}
course_about_template = 'courseware/course_about.html'
@@ -985,7 +975,10 @@ def _progress(request, course_key, student_id):
student_id = int(student_id)
# Check for ValueError if 'student_id' cannot be converted to integer.
except ValueError:
- raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
+ try:
+ user_by_username = User.objects.get(username=student_id)
+ except User.DoesNotExist:
+ raise Http404
course = get_course_with_access(request.user, 'load', course_key)
@@ -1007,9 +1000,12 @@ def _progress(request, course_key, student_id):
if not has_access_on_students_profiles:
raise Http404
try:
- student = User.objects.get(id=student_id)
+ if user_by_username:
+ student = user_by_username
+ else:
+ student = User.objects.get(id=student_id)
except User.DoesNotExist:
- raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
+ raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
# NOTE: To make sure impersonation by instructor works, use
# student instead of request.user in the rest of the function.
diff --git a/lms/djangoapps/edxnotes/helpers.py b/lms/djangoapps/edxnotes/helpers.py
index 17705f835e9d..f81947ce1e78 100644
--- a/lms/djangoapps/edxnotes/helpers.py
+++ b/lms/djangoapps/edxnotes/helpers.py
@@ -29,6 +29,7 @@
from lms.lib.utils import get_parent_unit
from openedx.core.djangoapps.oauth_dispatch.jwt import create_jwt_for_user
from openedx.core.djangolib.markup import Text
+from openedx.features.course_experience.url_helpers import get_courseware_url
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
@@ -256,16 +257,8 @@ def get_block_context(course, block):
course = block.get_parent()
block_dict['index'] = get_index(block_dict['location'], course.children)
elif block.category == 'vertical':
- section = block.get_parent()
- chapter = section.get_parent()
- # Position starts from 1, that's why we add 1.
- position = get_index(str(block.location), section.children) + 1
- block_dict['url'] = reverse('courseware_position', kwargs={
- 'course_id': str(course.id),
- 'chapter': chapter.url_name,
- 'section': section.url_name,
- 'position': position,
- })
+ # Use the MFE-aware URL generator instead of always using the legacy URL format
+ block_dict['url'] = get_courseware_url(block.location)
if block.category in ('chapter', 'sequential'):
block_dict['children'] = [str(child) for child in block.children]
diff --git a/lms/djangoapps/grades/rest_api/serializers.py b/lms/djangoapps/grades/rest_api/serializers.py
index b5c757f31bc6..c3969a947ff4 100644
--- a/lms/djangoapps/grades/rest_api/serializers.py
+++ b/lms/djangoapps/grades/rest_api/serializers.py
@@ -57,7 +57,7 @@ class StudentGradebookEntrySerializer(serializers.Serializer):
external_user_key = serializers.CharField(required=False)
percent = serializers.FloatField()
section_breakdown = SectionBreakdownSerializer(many=True)
-
+ profile_name = serializers.CharField(required=False, allow_null=True)
class SubsectionGradeOverrideSerializer(serializers.Serializer):
"""
diff --git a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py
index c295563da565..14c5e38c6313 100644
--- a/lms/djangoapps/grades/rest_api/v1/gradebook_views.py
+++ b/lms/djangoapps/grades/rest_api/v1/gradebook_views.py
@@ -507,6 +507,8 @@ def _gradebook_entry(self, user, course, graded_subsections, course_grade):
kwargs=dict(course_id=str(course.id), student_id=user.id)
)
user_entry['user_id'] = user.id
+ profile = getattr(user, 'profile', None)
+ user_entry['profile_name'] = getattr(profile, 'name', None)
def is_masters_student():
# If this is a multiple-user lookup (didn't use the username param) we insert
@@ -654,7 +656,7 @@ def get(self, request, course_key): # lint-amnesty, pylint: disable=too-many-st
# TODO: In django 3.0+, we can directly filter on this 'exists' rather than annotating
q_objects.append(Q(has_excluded_role=False))
entries = []
- related_models = ['user']
+ related_models = ['user', 'user__profile']
users = self._paginate_users(course_key, q_objects, related_models, annotations=annotations)
users_counts = self._get_users_counts(course_key, q_objects, annotations=annotations)
diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py
index 5358af370897..b9cb4ca19454 100644
--- a/lms/djangoapps/instructor_task/tasks_helper/grades.py
+++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py
@@ -462,20 +462,26 @@ def get_enrolled_learners_for_course(course_id, verified_only=False):
}
if verified_only:
filter_kwargs['courseenrollment__mode'] = CourseMode.VERIFIED
-
- user_ids_list = get_user_model().objects.filter(**filter_kwargs).values_list('id', flat=True).order_by('id')
- user_chunks = grouper(user_ids_list)
- for user_ids in user_chunks:
- user_ids = [user_id for user_id in user_ids if user_id is not None]
- min_id = min(user_ids)
- max_id = max(user_ids)
- users = get_user_model().objects.filter(
- id__gte=min_id,
- id__lte=max_id,
- **filter_kwargs
- ).select_related('profile')
-
- yield users
+ #
+ users_qs = get_user_model().objects.filter(**filter_kwargs).select_related('profile').order_by('id')
+ chunk_size = 100
+
+ for i in range(0, users_qs.count(), chunk_size):
+ yield users_qs[i:i + chunk_size]
+ #
+ # user_ids_list = get_user_model().objects.filter(**filter_kwargs).values_list('id', flat=True).order_by('id')
+ # user_chunks = grouper(user_ids_list)
+ # for user_ids in user_chunks:
+ # user_ids = [user_id for user_id in user_ids if user_id is not None]
+ # min_id = min(user_ids)
+ # max_id = max(user_ids)
+ # users = get_user_model().objects.filter(
+ # id__gte=min_id,
+ # id__lte=max_id,
+ # **filter_kwargs
+ # ).select_related('profile')
+ #
+ # yield users
return get_enrolled_learners_for_course(
course_id=self.context.course_id,
@@ -743,7 +749,7 @@ def _error_headers(self):
def _problem_grades_header(self):
"""Problem Grade report header."""
- return OrderedDict([('id', 'Student ID'), ('email', 'Email'), ('username', 'Username')])
+ return OrderedDict([('id', 'Student ID'), ('email', 'Email'),('name', 'Name'), ('username', 'Username')])
def _rows_for_users(self, users):
"""
@@ -761,10 +767,20 @@ def _rows_for_users(self, users):
# There was an error grading this student.
if not err_msg:
err_msg = 'Unknown error'
+ # error_rows.append(
+ # [student.id, student.email, student.username] +
+ # [err_msg]
+ # )
error_rows.append(
- [student.id, student.email, student.username] +
+ [
+ student.id,
+ student.email,
+ getattr(student.profile, "name", ""), # ← ДОБАВИЛИ
+ student.username
+ ] +
[err_msg]
)
+
continue
earned_possible_values = []
@@ -780,8 +796,18 @@ def _rows_for_users(self, users):
earned_possible_values.append(['Not Attempted', problem_score.possible])
enrollment_status = _user_enrollment_status(student, self.context.course_id)
+ # success_rows.append(
+ # [student.id, student.email, student.username] +
+ # [enrollment_status, course_grade.percent] +
+ # _flatten(earned_possible_values)
+ # )
success_rows.append(
- [student.id, student.email, student.username] +
+ [
+ student.id,
+ student.email,
+ getattr(student.profile, "name", ""), # ← ДОБАВИЛИ
+ student.username
+ ] +
[enrollment_status, course_grade.percent] +
_flatten(earned_possible_values)
)
diff --git a/lms/djangoapps/news/admin.py b/lms/djangoapps/news/admin.py
new file mode 100644
index 000000000000..8443d1c799ac
--- /dev/null
+++ b/lms/djangoapps/news/admin.py
@@ -0,0 +1,7 @@
+from django.contrib import admin
+from .models import News
+
+@admin.register(News)
+class NewsAdmin(admin.ModelAdmin):
+ list_display = ('title', 'created_at')
+ search_fields = ('title', 'content')
\ No newline at end of file
diff --git a/lms/djangoapps/news/forms.py b/lms/djangoapps/news/forms.py
new file mode 100644
index 000000000000..98cbb2bb4df6
--- /dev/null
+++ b/lms/djangoapps/news/forms.py
@@ -0,0 +1,10 @@
+from django import forms
+from .models import News
+
+class NewsForm(forms.ModelForm):
+ class Meta:
+ model = News
+ fields = ['title', 'content', 'image']
+ widgets = {
+ 'content': forms.Textarea(attrs={'rows': 10}),
+ }
\ No newline at end of file
diff --git a/lms/djangoapps/news/migrations/0001_initial.py b/lms/djangoapps/news/migrations/0001_initial.py
new file mode 100644
index 000000000000..67310398ac14
--- /dev/null
+++ b/lms/djangoapps/news/migrations/0001_initial.py
@@ -0,0 +1,30 @@
+# Generated by Django 4.2.20 on 2025-09-30 11:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='News',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=200, verbose_name='Заголовок')),
+ ('content', models.TextField(verbose_name='Содержание')),
+ ('image', models.ImageField(blank=True, null=True, upload_to='articles/', verbose_name='Изображение')),
+ ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
+ ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
+ ],
+ options={
+ 'verbose_name': 'Новость',
+ 'verbose_name_plural': 'Новости',
+ 'ordering': ['-created_at'],
+ },
+ ),
+ ]
diff --git a/lms/djangoapps/news/migrations/__init__.py b/lms/djangoapps/news/migrations/__init__.py
new file mode 100644
index 000000000000..e69de29bb2d1
diff --git a/lms/djangoapps/news/models.py b/lms/djangoapps/news/models.py
new file mode 100644
index 000000000000..a8f19eb65335
--- /dev/null
+++ b/lms/djangoapps/news/models.py
@@ -0,0 +1,22 @@
+from django.db import models
+
+class News(models.Model):
+ title = models.CharField(max_length=200, verbose_name="Заголовок")
+ content = models.TextField(verbose_name="Содержание")
+ image = models.ImageField(
+ upload_to='articles/',
+ verbose_name="Изображение",
+ blank=True,
+ null=True
+ )
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
+ updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
+
+ class Meta:
+ verbose_name = "Новость"
+ verbose_name_plural = "Новости"
+ ordering = ['-created_at']
+
+ def __str__(self):
+ return self.title
+
diff --git a/lms/djangoapps/news/views.py b/lms/djangoapps/news/views.py
new file mode 100644
index 000000000000..98190738a0b5
--- /dev/null
+++ b/lms/djangoapps/news/views.py
@@ -0,0 +1,442 @@
+from django.shortcuts import render, get_object_or_404, redirect
+from django.urls import reverse
+from common.djangoapps.edxmako.shortcuts import render_to_response
+from .models import News
+from .forms import NewsForm
+from django.contrib.auth.decorators import user_passes_test
+from common.djangoapps.student.models import UserProfile
+
+import json
+from django.utils import timezone
+from collections import Counter
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
+# from ..commerce.api.v1.models import Course
+import logging
+import jwt
+import urllib.parse
+import random
+from datetime import datetime, timedelta
+
+from django.http import HttpResponseRedirect
+
+from django.http import JsonResponse
+from django.views.decorators.csrf import csrf_exempt
+from django.conf import settings
+
+
+from django.db.models.functions import ExtractYear
+from django.db.models import Count
+
+logger = logging.getLogger(__name__)
+
+def news_list(request):
+ news = News.objects.all()
+ context = {
+ 'news_list': news,
+ 'create_url': reverse('news_create'),
+ }
+ return render_to_response('news/list.html', context, request=request)
+
+def news_detail(request, news_id): # используем news_id
+ news = get_object_or_404(News, pk=news_id) # все равно используем pk для поиска
+ context = {
+ 'news': news,
+ 'list_url': reverse('news_list'),
+ }
+ return render_to_response('news/detail.html', context, request=request)
+
+
+@user_passes_test(lambda u: u.is_staff)
+def news_create(request):
+ if request.method == 'POST':
+ form = NewsForm(request.POST, request.FILES)
+ if form.is_valid():
+ form.save()
+ return redirect('news_list')
+ else:
+ form = NewsForm()
+
+ context = {
+ 'form': form,
+ 'list_url': reverse('news_list'),
+ }
+ return render_to_response('news/form.html', context, request=request)
+
+#
+from django.utils.translation import get_language
+
+FACULTY_TRANSLATIONS = {
+ "Биология и биотехнология": {
+ "kk": "Биология және биотехнология",
+ "en": "Biology and Biotechnology",
+ },
+ "Востоковедение": {
+ "kk": "Шығыстану",
+ "en": "Oriental Studies",
+ },
+ "Высшая школа экономики и бизнеса": {
+ "kk": "Экономика және бизнес жоғары мектебі",
+ "en": "Higher School of Economics and Business",
+ },
+ "Довузовское образование": {
+ "kk": "Жоғары оқу орнына дейінгі білім беру",
+ "en": "Pre-university Education",
+ },
+ "Журналистика": {
+ "kk": "Журналистика",
+ "en": "Journalism",
+ },
+ "Офис академических и цифровых инноваций": {
+ "kk": "Академиялық және цифрлық инновациялар кеңсесі",
+ "en": "Office of Academic and Digital Innovations",
+ },
+ "Информационные технологии и искусственный интеллект": {
+ "kk": "Ақпараттық технологиялар және жасанды интеллект",
+ "en": "Information Technology and Artificial Intelligence",
+ },
+ "История": {
+ "kk": "Тарих",
+ "en": "History",
+ },
+ "Кластер инжиниринга и наукоемких технологий": {
+ "kk": "Инжиниринг және жоғары технологиялар кластері",
+ "en": "Cluster of Engineering and High Technologies",
+ },
+ "Механико-математический": {
+ "kk": "Механика-математика",
+ "en": "Mechanics and Mathematics",
+ },
+ "Физико-технический": {
+ "kk": "Физика-техникалық",
+ "en": "Physics and Technology",
+ },
+ "Филологический": {
+ "kk": "Филология",
+ "en": "Philology",
+ },
+ "Философии и политологии": {
+ "kk": "Философия және саясаттану",
+ "en": "Philosophy and Political Science",
+ },
+ "Химии и химической технологии": {
+ "kk": "Химия және химиялық технологиялар",
+ "en": "Chemistry and Chemical Technology",
+ },
+ "Юридический": {
+ "kk": "Заң",
+ "en": "Law",
+ },
+ "Международных отношений": {
+ "kk": "Халықаралық қатынастар",
+ "en": "International Relations",
+ },
+ "Медицины и здравоохранения": {
+ "kk": "Медицина және денсаулық сақтау",
+ "en": "Medicine and Healthcare",
+ },
+}
+
+DIRECTION_TRANSLATIONS = {
+ "Социальные науки, журналистика и информация": {
+ "kk": "Әлеуметтік ғылымдар, журналистика және ақпарат",
+ "en": "Social sciences, Journalism and Information",
+ },
+ "Естественные науки, математика и статистика": {
+ "kk": "Жаратылыстану ғылымдары, математика және статистика",
+ "en": "Natural Sciences, Mathematics and Statistics",
+ },
+ "Искусство и гуманитарные науки": {
+ "kk": "Өнер және гуманитарлық ғылымдар",
+ "en": "Arts and Humanities",
+ },
+ "Бизнес, управление и право": {
+ "kk": "Бизнес, басқару және құқық",
+ "en": "Business, Management and Law",
+ },
+ "Педагогические науки": {
+ "kk": "Педагогикалық ғылымдар",
+ "en": "Pedagogical sciences",
+ },
+ "Информационно-коммуникационные технологии": {
+ "kk": "Ақпараттық-коммуникациялық технологиялар",
+ "en": "Information and communication technologies",
+ },
+ "Инженерные, обрабатывающие и строительные отрасли": {
+ "kk": "Инженерлік, өңдеу және құрылыс салалары",
+ "en": "Engineering, manufacturing and construction branches",
+ },
+ "Здравоохранение": {
+ "kk": "Денсаулық сақтау",
+ "en": "Healthcare",
+ },
+}
+
+
+def normalize_language_code():
+ language = get_language() or "ru"
+ return language.split("-")[0].split("_")[0]
+
+
+def translate_faculty(value):
+ if not value:
+ return ""
+
+ language = normalize_language_code()
+
+ if language == "ru":
+ return value
+
+ return FACULTY_TRANSLATIONS.get(value, {}).get(language, value)
+
+
+def translate_direction(value):
+ if not value:
+ return ""
+
+ language = normalize_language_code()
+
+ if language == "ru":
+ return value
+
+ return DIRECTION_TRANSLATIONS.get(value, {}).get(language, value)
+#
+def analyze(request):
+ course_org_filter = ["Test_kaznu", "rty", "123", "AI Tools in Action: Boosting Productivity with Modern Workflows", "Demo"]
+
+ now = timezone.now()
+ today = now.date()
+ current_year = today.year
+
+ max_valid_end = now + timedelta(days=366)
+
+ base_courses_qs = (
+ CourseOverview.objects
+ .exclude(org__in=course_org_filter)
+
+ # курс еще не должен закончиться
+ .exclude(end__isnull=True)
+ # .exclude(end__lt=now)
+
+ # скрываем слишком долгие/ошибочные курсы, например до 2028 года
+ .exclude(end__gt=max_valid_end)
+
+ # убираем пустые названия
+ .filter(start__lte=now)
+ .exclude(display_name__isnull=True)
+ .exclude(display_name="")
+ .order_by("display_name", "-start", "-id")
+ )
+
+ unique_courses = {}
+ for course in base_courses_qs:
+ if course.display_name not in unique_courses:
+ unique_courses[course.display_name] = course
+
+ courses = list(unique_courses.values())
+
+ current_year_courses = [
+ course for course in courses
+ if course.start and course.start.year == current_year
+ ]
+ # Rewrite code to test it #
+ current_year_courses = courses
+ #
+ max_year = current_year + 1
+
+ courses_by_year_qs = (
+ CourseOverview.objects
+ .exclude(org__in=course_org_filter)
+ .exclude(start__isnull=True)
+ .filter(start__year__lte=max_year)
+ .annotate(year=ExtractYear("start"))
+ .values("year")
+ .annotate(total=Count("id"))
+ .order_by("year")
+ )
+ courses_by_year = [
+ {"year": row["year"], "total": row["total"]}
+ for row in courses_by_year_qs
+ ]
+
+ #
+ faculty_counter = Counter(
+ course.faculty for course in courses
+ if course.faculty
+ )
+
+ directions_counter = Counter(
+ course.directions for course in courses
+ if course.directions
+ )
+
+ language_counter = Counter(
+ course.language for course in courses
+ if course.language
+ )
+
+ courses_by_faculty = [
+ {"faculty": faculty, "total": total}
+ for faculty, total in faculty_counter.most_common(12)
+ ]
+
+ courses_by_directions = [
+ {"directions": directions, "total": total}
+ for directions, total in directions_counter.most_common(12)
+ ]
+
+ courses_by_lang = [
+ {"language": language, "total": total}
+ for language, total in language_counter.most_common()
+ ]
+
+ top_courses = sorted(
+ current_year_courses,
+ key=lambda course: course.start,
+ reverse=True
+ )
+
+ course_run_counts = dict(
+ CourseOverview.objects
+ .exclude(org__in=course_org_filter)
+ .exclude(display_name__isnull=True)
+ .exclude(display_name="")
+ .exclude(start__isnull=True)
+ .values("display_name")
+ .annotate(total=Count("id", distinct=True))
+ .values_list("display_name", "total")
+ )
+
+ courses_json = [
+ {
+ "id": str(course.id),
+ "display_name": course.display_name or str(course.id),
+ "faculty": translate_faculty(course.faculty),
+ "directions": translate_direction(course.directions),
+ "language": course.language or "",
+ "start": course.start.strftime("%d.%m.%Y") if course.start else "",
+ "run_count": course_run_counts.get(course.display_name, 1),
+ "url": "/courses/{}/about".format(course.id),
+ }
+ for course in top_courses
+ ]
+
+ context = {
+ "courses_count": len(courses),
+ "current_year": current_year,
+ "current_year_courses_count": len(current_year_courses),
+ "faculty_count": len(set(course.faculty for course in courses if course.faculty)),
+ "directions_count": len(set(course.directions for course in courses if course.directions)),
+ "generated_at": timezone.localtime().strftime("%d.%m.%Y %H:%M"),
+
+ "language_summary": [
+ {"label": row["language"], "total": row["total"]}
+ for row in courses_by_lang
+ ],
+
+ "faculty_labels": json.dumps(
+ [translate_faculty(row["faculty"]) for row in courses_by_faculty],
+ ensure_ascii=False
+ ),
+ "faculty_data": json.dumps([row["total"] for row in courses_by_faculty]),
+
+ "directions_labels": json.dumps(
+ [translate_direction(row["directions"]) for row in courses_by_directions],
+ ensure_ascii=False
+ ),
+ "directions_data": json.dumps([row["total"] for row in courses_by_directions]),
+
+ "year_labels": json.dumps([row["year"] for row in courses_by_year]),
+ "year_data": json.dumps([row["total"] for row in courses_by_year]),
+
+ "courses_json": json.dumps(courses_json, ensure_ascii=False),
+ }
+
+ return render_to_response("news/analyze.html", context, request=request)
+
+
+
+PROCTORING_URL = "https://farabi-proctoring.kaznu.kz/integration/simple/kaznu_open/start/"
+
+def go_to_exam(request):
+ SECRET_KEY = str(settings.PROCTORING_API_KEY)
+ # ✅ 1. Берем данные из frontend
+ user_id = request.user.id
+ username = request.user.username
+ unit_url = request.GET.get("unit_url", "/")
+ course_name = request.GET.get("course_name", "empty")
+ section_name = request.GET.get("section_name", "empty")
+
+
+ # можно взять реальные данные если нужно
+ try:
+ name = request.user.profile.name.strip()
+ parts = name.split(maxsplit=1)
+
+ firstname = parts[0] if len(parts) > 0 else ""
+ lastname = parts[1] if len(parts) > 1 else ""
+ except:
+ firstname, lastname = "None", "None"
+
+ exam_id = random.randint(10**7, 10**8 - 1)
+ session_id = random.randint(10**10, 10**11 - 1)
+ exam_name = f"Экзамен по {course_name}"
+ request.session["proctoring_session_id"] = session_id
+ logger.info(f"my-log: exam start {session_id}")
+
+
+ # 2. Время
+ now = datetime.utcnow()
+ start_iso = now.isoformat() + "Z"
+ end_iso = (now + timedelta(hours=2)).isoformat() + "Z"
+
+ # ✅ 3. Payload с динамическим возвратом
+ payload = {
+ "userId": user_id,
+ "lastName": lastname,
+ "firstName": firstname,
+ "thirdName": username,
+ "language": "ru",
+ "accountName": "kaznu_open",
+ "examId": exam_id,
+ "examName": exam_name,
+ "duration": 30,
+ "schedule": False,
+ "proctoring": "online",
+ "examDesc": f"Курс: {course_name} Модуль: {section_name}
Ссылка на задание {unit_url}",
+ "rules": {
+ "websites": True,
+ "look_away": True,
+ "move_away": False,
+ "voices": True,
+ },
+ "startDate": start_iso,
+ "endDate": end_iso,
+ "sessionId": session_id,
+
+ # 🔥 ВАЖНО: возвращаем туда откуда пришли
+ "sessionUrl": unit_url,
+ "redirectUrl": unit_url,
+ }
+
+ # 4. JWT
+ token = jwt.encode(payload, SECRET_KEY, algorithm="HS256")
+
+ # 5. URL
+ encoded_token = urllib.parse.quote(token)
+ final_url = f"{PROCTORING_URL}?token={encoded_token}"
+
+ logger.info(f"my-log: request get {request.GET}")
+
+ return HttpResponseRedirect(final_url)
+
+
+def finish_exam(request):
+ session_id = request.session.get("proctoring_session_id")
+ logger.info(f"my-log: exam finished {session_id}")
+
+ redirect_url = request.GET.get("redirectUrl", "/")
+ logger.info(f"my-log: exam finished {redirect_url}")
+
+ url = f"https://farabi-proctoring.kaznu.kz/integration/simple/kaznu_open/finish/{session_id}/?redirectUrl={redirect_url}"
+
+ return HttpResponseRedirect(url)
diff --git a/lms/djangoapps/static_template_view/urls.py b/lms/djangoapps/static_template_view/urls.py
index 231913fbfcc1..0965a51b1244 100644
--- a/lms/djangoapps/static_template_view/urls.py
+++ b/lms/djangoapps/static_template_view/urls.py
@@ -5,6 +5,7 @@
from django.conf import settings
from django.urls import path, re_path
+from django.views.generic.base import RedirectView
from lms.djangoapps.static_template_view import views
@@ -15,10 +16,15 @@
path('faq', views.render, {'template': 'faq.html'}, name="faq"),
path('help', views.render, {'template': 'help.html'}, name="help_edx"),
path('jobs', views.render, {'template': 'jobs.html'}, name="jobs"),
- path('news', views.render, {'template': 'news.html'}, name="news"),
path('press', views.render, {'template': 'press.html'}, name="press"),
path('media-kit', views.render, {'template': 'media-kit.html'}, name="media-kit"),
path('copyright', views.render, {'template': 'copyright.html'}, name="copyright"),
+ path('competition', views.render, {'template': 'competition.html'}, name="competition"),
+ path('catalog_transfer', views.render, {'template': 'catalog_transfer.html'}, name="catalog_transfer"),
+ path('catalog', RedirectView.as_view(pattern_name='catalog_transfer', permanent=False), name="catalog"),
+ path('author', views.render, {'template': 'author.html'}, name="author"),
+ path('detect', views.render, {'template': 'detect.html'}, name="detect"),
+ path('honor_code', views.render, {'template': 'honor_code.html'}, name="honor_code"),
# Press releases
re_path(r'^press/([_a-zA-Z0-9-]+)$', views.render_press_release, name='press_release'),
diff --git a/lms/djangoapps/univerapi/admin.py b/lms/djangoapps/univerapi/admin.py
new file mode 100644
index 000000000000..ea5d68b7c457
--- /dev/null
+++ b/lms/djangoapps/univerapi/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/lms/djangoapps/univerapi/views.py b/lms/djangoapps/univerapi/views.py
new file mode 100644
index 000000000000..a58bd64c06a6
--- /dev/null
+++ b/lms/djangoapps/univerapi/views.py
@@ -0,0 +1,194 @@
+import jwt
+import requests
+from django.contrib.auth import login, authenticate, get_user_model
+from django.http import HttpResponseRedirect
+from django.views.decorators.csrf import csrf_exempt
+from rest_framework.views import APIView
+from rest_framework.response import Response
+from rest_framework import status
+from common.djangoapps.student.models import UserProfile
+
+SECRET_KEY = "$ecRet@3#$2958GPIs!1"
+User = get_user_model()
+
+
+class UniverTestView(APIView):
+ authentication_classes = []
+ permission_classes = []
+
+ @csrf_exempt
+ def get(self, request):
+ auth_token = request.GET.get('auth')
+ if not auth_token:
+ return Response({'error': 'Missing auth token'}, status=status.HTTP_400_BAD_REQUEST)
+
+ try:
+ decoded = jwt.decode(auth_token, SECRET_KEY, algorithms=['HS256'])
+ uname = decoded.get('uname')
+ upwd = decoded.get('upwd')
+
+ if not uname or not upwd:
+ return Response({'error': 'Invalid token payload'}, status=status.HTTP_400_BAD_REQUEST)
+
+ username = uname
+ email_value = f"{username}@open.edu.kz"
+
+ # Получаем данные профиля из Univer API
+ surname, name, gender, stage, birth_year = decode_token_and_fetch_profile(auth_token)
+
+ # Проверка пользователя
+ user = User.objects.filter(username=username).first()
+
+ if not user:
+ # Создание нового пользователя
+ user = User.objects.create(username=username, email=email_value)
+ user.set_password(upwd)
+ user.save()
+
+ UserProfile.objects.create(
+ user=user,
+ name=f"{name} {surname}",
+ country='KZ',
+ gender=gender,
+ level_of_education=stage,
+ year_of_birth=birth_year,
+ mailing_address='Kaznu',
+ goals='Цель обучаться на платформе ОпенКазну'
+ )
+ else:
+ # Обновляем пароль при изменении
+ if not user.check_password(upwd):
+ user.set_password(upwd)
+ user.save()
+
+ # Проверка и обновление профиля
+ profile, created = UserProfile.objects.get_or_create(user=user)
+ updated = False
+
+ if profile.name != f"{name} {surname}":
+ profile.name = f"{name} {surname}"
+ updated = True
+ if profile.gender != gender:
+ profile.gender = gender
+ updated = True
+ if profile.level_of_education != stage:
+ profile.level_of_education = stage
+ updated = True
+ if profile.year_of_birth != birth_year:
+ profile.year_of_birth = birth_year
+ updated = True
+ if profile.country != 'KZ':
+ profile.country = 'KZ'
+ updated = True
+ if profile.mailing_address != 'Kaznu':
+ profile.mailing_address = 'Kaznu'
+ updated = True
+
+ if updated:
+ profile.save()
+
+ # Авторизация
+ auth_user = authenticate(username=username, password=upwd)
+ if auth_user is None:
+ return Response({'error': 'Authentication failed'}, status=status.HTTP_401_UNAUTHORIZED)
+
+ login(request, auth_user)
+ return HttpResponseRedirect('/dashboard')
+
+ except jwt.ExpiredSignatureError:
+ return Response({'error': 'Token expired'}, status=status.HTTP_401_UNAUTHORIZED)
+ except jwt.InvalidTokenError:
+ return Response({'error': 'Invalid token'}, status=status.HTTP_401_UNAUTHORIZED)
+ except Exception as e:
+ return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+
+def decode_token_and_fetch_profile(token, secret_key=SECRET_KEY):
+ """Декодирование токена и получение профиля пользователя с Univer API"""
+ try:
+ decoded = jwt.decode(token, secret_key, algorithms=['HS256'])
+ except jwt.InvalidSignatureError:
+ raise SystemExit("Ошибка: неверная подпись JWT")
+ except jwt.DecodeError as e:
+ raise SystemExit(f"Ошибка при декодировании JWT: {e}")
+
+ uname = decoded.get('uname')
+ upwd = decoded.get('upwd')
+ if not uname or not upwd:
+ raise SystemExit("Ошибка: в токене нет 'uname' или 'upwd'")
+
+ session = requests.Session()
+ resp = session.post(
+ "https://univerapi.kaznu.kz/user/loginMoodle",
+ data={'login': uname, 'password': upwd},
+ )
+ resp.raise_for_status()
+ data = resp.json()
+ if data.get('code') != 0:
+ raise SystemExit(f"loginMoodle вернул: {data.get('message', '')}")
+
+ def find_value(lst, key):
+ for d in lst:
+ if key in d and ':' in d[key]:
+ return d[key].split(':', 1)[1].strip()
+ return None
+
+ # 1. Пробуем студентский профиль
+ try:
+ student_resp = session.get("https://univerapi.kaznu.kz/student/profile")
+ if student_resp.status_code == 200:
+ student_data = student_resp.json()
+ if student_data.get('code') == 0 and 'data' in student_data:
+ info_list, personal_list = student_data['data']
+
+ surname = find_value(personal_list, 'sname')
+ name = find_value(personal_list, 'name')
+
+ raw_sex = (find_value(personal_list, 'sex') or '').lower()
+ if u'муж' in raw_sex:
+ gender = 'm'
+ elif u'жен' in raw_sex:
+ gender = 'f'
+ else:
+ gender = 'o'
+
+ raw_stage = (find_value(info_list, 'stage') or '').lower()
+ if u'бакалав' in raw_stage:
+ stage = 'b'
+ elif u'магис' in raw_stage:
+ stage = 'm'
+ elif u'доктор' in raw_stage:
+ stage = 'p'
+ else:
+ stage = 'none'
+
+ birth_year = 1995
+ return surname, name, gender, stage, birth_year
+ except:
+ pass
+
+ # 2. Пробуем профиль преподавателя
+ try:
+ teacher_resp = session.get("https://univerapi.kaznu.kz/teacher/profile")
+ if teacher_resp.status_code == 200:
+ teacher_data = teacher_resp.json()
+ if teacher_data.get('code') == 0 and 'data' in teacher_data:
+ teacher_profile = teacher_data['data'][0]
+ surname = teacher_profile.get('sname', '')
+ name = teacher_profile.get('name', '')
+ gender = 'o'
+ stage = 'none'
+ birth_year = None
+ if 'dateOfBirth' in teacher_profile:
+ try:
+ birth_year = int(teacher_profile['dateOfBirth'].split('.')[-1])
+ except (ValueError, IndexError):
+ pass
+ return surname, name, gender, stage, birth_year
+ except:
+ pass
+
+
+ return "Сотрудник", "Казну", "o", "none", 1995
+
+
+ return surname, name, gender, stage, birth_year
diff --git a/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py b/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py
index 04f75ef42439..0bfef6d0ac91 100644
--- a/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py
+++ b/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py
@@ -188,10 +188,11 @@ def send_verification_expiry_email(self, batch_verifications, email_config):
return True
site = Site.objects.get_current()
+ account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
message_context = get_base_template_context(site)
message_context.update({
'platform_name': settings.PLATFORM_NAME,
- 'lms_verification_link': f'{settings.ACCOUNT_MICROFRONTEND_URL}/id-verification',
+ 'lms_verification_link': f'{account_base_url}/id-verification',
'help_center_link': settings.ID_VERIFICATION_SUPPORT_LINK
})
diff --git a/lms/djangoapps/verify_student/services.py b/lms/djangoapps/verify_student/services.py
index 95dbccf0d59d..5caede3dab83 100644
--- a/lms/djangoapps/verify_student/services.py
+++ b/lms/djangoapps/verify_student/services.py
@@ -251,7 +251,8 @@ def get_verify_location(cls, course_id=None):
Returns a string:
Returns URL for IDV on Account Microfrontend
"""
- location = f'{settings.ACCOUNT_MICROFRONTEND_URL}/id-verification'
+ account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
+ location = f'{account_base_url}/id-verification'
if course_id:
location += f'?course_id={quote(str(course_id))}'
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index 1b6a47bee879..deda08e0c741 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -1128,7 +1128,8 @@ def results_callback(request): # lint-amnesty, pylint: disable=too-many-stateme
log.info("[COSMO-184] Denied verification for receipt_id={receipt_id}.".format(receipt_id=receipt_id))
attempt.deny(json.dumps(reason), error_code=error_code)
- reverify_url = f'{settings.ACCOUNT_MICROFRONTEND_URL}/id-verification'
+ account_base_url = (settings.ACCOUNT_MICROFRONTEND_URL or "").rstrip('/')
+ reverify_url = f'{account_base_url}/id-verification'
verification_status_email_vars['reasons'] = reason
verification_status_email_vars['reverify_url'] = reverify_url
verification_status_email_vars['faq_url'] = settings.ID_VERIFICATION_SUPPORT_LINK
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 763bd83b9d8e..4b93d72f6977 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1985,7 +1985,7 @@ def _make_mako_template_dirs(settings):
('id', 'Bahasa Indonesia'), # Indonesian
('it-it', 'Italiano (Italia)'), # Italian (Italy)
('ja-jp', '日本語 (日本)'), # Japanese (Japan)
- ('kk-kz', 'қазақ тілі (Қазақстан)'), # Kazakh (Kazakhstan)
+ ('kk', 'Қазақша'), # Kazakh (Kazakhstan)
('km-kh', 'ភាសាខ្មែរ (កម្ពុជា)'), # Khmer (Cambodia)
('kn', 'ಕನ್ನಡ'), # Kannada
('ko-kr', '한국어 (대한민국)'), # Korean (Korea)
@@ -3033,6 +3033,8 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# - Make it a plugin (which are auto-registered) and add it to the EDXAPP_PRIVATE_REQUIREMENTS configuration variable
# (See https://github.com/openedx/edx-django-utils/tree/master/edx_django_utils/plugins)
INSTALLED_APPS = [
+ 'lms.djangoapps.univerapi',
+ 'lms.djangoapps.news',
# Standard ones that are always installed...
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -3441,7 +3443,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# It's POST endpoint is rate-limited up to 30 requests per IP Address in a week by default.
# It was introduced because an attacker can guess or brute force a series of names to enumerate valid users.
# .. setting_tickets: https://github.com/openedx/edx-platform/pull/24664
-REGISTRATION_VALIDATION_RATELIMIT = '30/7d'
+REGISTRATION_VALIDATION_RATELIMIT = '10000/7d'
# .. setting_name: REGISTRATION_RATELIMIT
# .. setting_default: 60/7d
@@ -3449,7 +3451,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# It's POST end-point is rate-limited up to 60 requests per IP Address in a week by default.
# Purpose of this setting is to restrict an attacker from registering numerous fake accounts.
# .. setting_tickets: https://github.com/openedx/edx-platform/pull/27060
-REGISTRATION_RATELIMIT = '60/7d'
+REGISTRATION_RATELIMIT = '10000/7d'
SWAGGER_SETTINGS = {
'DEFAULT_INFO': 'openedx.core.apidocs.api_info',
@@ -4881,7 +4883,7 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# .. toggle_use_cases: open_edx
# .. toggle_creation_date: 2018-01-08
# .. toggle_tickets: https://github.com/openedx/edx-platform/pull/16951
-RATELIMIT_ENABLE = True
+RATELIMIT_ENABLE = False
# .. setting_name: RATELIMIT_RATE
# .. setting_default: 120/m
@@ -4892,13 +4894,13 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring
# .. setting_use_cases: open_edx
# .. setting_creation_date: 2018-01-08
# .. setting_tickets: https://github.com/openedx/edx-platform/pull/16951
-RATELIMIT_RATE = '120/m'
+RATELIMIT_RATE = '1200/m'
##### LOGISTRATION RATE LIMIT SETTINGS #####
-LOGISTRATION_RATELIMIT_RATE = '100/5m'
-LOGISTRATION_PER_EMAIL_RATELIMIT_RATE = '30/5m'
-LOGISTRATION_API_RATELIMIT = '20/m'
-LOGIN_AND_REGISTER_FORM_RATELIMIT = '100/5m'
+LOGISTRATION_RATELIMIT_RATE = '6000/5m'
+LOGISTRATION_PER_EMAIL_RATELIMIT_RATE = '300/5m'
+LOGISTRATION_API_RATELIMIT = '1000/m'
+LOGIN_AND_REGISTER_FORM_RATELIMIT = '1000000/5m'
RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT = '30/7d'
RESET_PASSWORD_API_RATELIMIT = '30/7d'
OPTIONAL_FIELD_API_RATELIMIT = '10/h'
diff --git a/lms/lib/courseware_search/lms_filter_generator.py b/lms/lib/courseware_search/lms_filter_generator.py
index b0c0564df48a..5b2592e4cd19 100644
--- a/lms/lib/courseware_search/lms_filter_generator.py
+++ b/lms/lib/courseware_search/lms_filter_generator.py
@@ -9,6 +9,7 @@
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.partition_schemes import RandomUserPartitionScheme
from common.djangoapps.student.models import CourseEnrollment
+from xmodule.course_block import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE
INCLUDE_SCHEMES = [CohortPartitionScheme, RandomUserPartitionScheme, ]
SCHEME_SUPPORTS_ASSIGNMENT = [RandomUserPartitionScheme, ]
@@ -63,6 +64,6 @@ def exclude_dictionary(self, **kwargs):
if not getattr(settings, "SEARCH_SKIP_INVITATION_ONLY_FILTERING", True):
exclude_dictionary['invitation_only'] = True
if not getattr(settings, "SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING", True):
- exclude_dictionary['catalog_visibility'] = 'none'
+ exclude_dictionary['catalog_visibility'] = [CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE]
return exclude_dictionary
diff --git a/lms/lib/courseware_search/test/test_lms_filter_generator.py b/lms/lib/courseware_search/test/test_lms_filter_generator.py
index 492cf64d8c30..8afa94f70fbf 100644
--- a/lms/lib/courseware_search/test/test_lms_filter_generator.py
+++ b/lms/lib/courseware_search/test/test_lms_filter_generator.py
@@ -6,6 +6,7 @@
from lms.lib.courseware_search.lms_filter_generator import LmsSearchFilterGenerator
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
+from xmodule.course_block import CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
@@ -139,3 +140,9 @@ def test_excludes_multi_orgs_within(self):
assert 'org' not in exclude_dictionary
assert 'org' in field_dictionary
assert ['TestSite3'] == field_dictionary['org']
+
+ @patch('django.conf.settings.SEARCH_SKIP_SHOW_IN_CATALOG_FILTERING', False)
+ def test_excludes_catalog_visibility(self):
+ _, _, exclude_dictionary = LmsSearchFilterGenerator.generate_field_filters(user=self.user)
+ assert 'catalog_visibility' in exclude_dictionary
+ assert exclude_dictionary['catalog_visibility'] == [CATALOG_VISIBILITY_ABOUT, CATALOG_VISIBILITY_NONE]
diff --git a/lms/static/images/favicon.ico b/lms/static/images/favicon.ico
index 0347608a33d7..c9feabafecc0 100644
Binary files a/lms/static/images/favicon.ico and b/lms/static/images/favicon.ico differ
diff --git a/lms/static/images/logo.png b/lms/static/images/logo.png
index 640d80b956fb..e355386c98d2 100644
Binary files a/lms/static/images/logo.png and b/lms/static/images/logo.png differ
diff --git a/lms/static/images/programs/sample-cert.png b/lms/static/images/programs/sample-cert.png
index 09de8897d3a2..4639a97f8cd0 100644
Binary files a/lms/static/images/programs/sample-cert.png and b/lms/static/images/programs/sample-cert.png differ
diff --git a/lms/static/images/sign1.png b/lms/static/images/sign1.png
new file mode 100644
index 000000000000..40e13c2af18c
Binary files /dev/null and b/lms/static/images/sign1.png differ
diff --git a/lms/static/images/sign2.jpg b/lms/static/images/sign2.jpg
new file mode 100644
index 000000000000..2aae0e1ca863
Binary files /dev/null and b/lms/static/images/sign2.jpg differ
diff --git a/lms/templates/certificates/_about-accomplishments.html b/lms/templates/certificates/_about-accomplishments.html
index be4d50a7ec9e..c847f75e4f7b 100644
--- a/lms/templates/certificates/_about-accomplishments.html
+++ b/lms/templates/certificates/_about-accomplishments.html
@@ -1,8 +1 @@
<%page expression_filter="h"/>
-
-
${_("Explore our collection of educational programs and start learning today")}
+
+
+
+
+
${_("Filter Courses")}
+
+
+
+
-
-
-
- % if course_discovery_enabled:
-
-
-
+
+
+
+ % if courses:
+ % for course in courses:
+ <%
+ from django.utils import timezone
+ now = timezone.now()
+ status = "completed" if course.end and course.end < now else "ongoing"
+ status_text = _('Completed') if status == "completed" else _('Ongoing')
+ %>
+
${_("Explore our collection of educational programs and start learning today")}
+
+
+
+
+
${_("Filter Courses")}
+
+
+
+
+
+
+
+
+
+
+
+
+ % if courses:
+ % for course in courses:
+ <%
+ from django.utils import timezone
+ now = timezone.now()
+ status = "completed" if course.end and course.end < now else "ongoing"
+ status_text = _('Completed') if status == "completed" else _('Ongoing')
+ %>
+
${_("Explore our collection of educational programs and start learning today")}
+
+
+
+ ${len(courses)}
+ ${_("Courses")}
+
+
+ ${len(set([c.language for c in courses if c.language]))}
+ ${_("Languages")}
+
+
+ ${len(set([c.faculty for c in courses if c.faculty]))}
+ ${_("Faculties")}
+
+
+
+
+
+
+
+
+
${_("Filter Courses")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ % if courses:
+ % for course in courses:
+ <%
+ from django.utils import timezone
+ now = timezone.now()
+ status = "completed" if course.end and course.end < now else "ongoing"
+ status_text = _('Completed') if status == "completed" else _('Ongoing')
+ %>
+
+
${_("Explore our collection of educational programs and start learning today")}
+
+
+
+
+
${_("Filter Courses")}
+
+
+
+
+
+
+
+
+
+
+
+
+ % if courses:
+ % for course in courses:
+ <%
+ from django.utils import timezone
+ now = timezone.now()
+ status = "completed" if course.end and course.end < now else "ongoing"
+ status_text = _('Completed') if status == "completed" else _('Ongoing')
+ %>
+