diff --git a/cms/djangoapps/contentstore/course_admin.py b/cms/djangoapps/contentstore/course_admin.py new file mode 100644 index 000000000000..efd988f73bac --- /dev/null +++ b/cms/djangoapps/contentstore/course_admin.py @@ -0,0 +1,535 @@ +"""Services for the private course administration page. + +Reruns use Studio's existing cloning task. SQL stores only the reservation, +enrollment modes and certificate configuration; learner records are never copied. +""" + +import json +import logging +import re +from copy import deepcopy +from datetime import datetime, timezone + +from celery import current_app +from django.core.exceptions import PermissionDenied, ValidationError +from django.db import IntegrityError, transaction +from django.utils.dateparse import parse_datetime +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.locator import CourseLocator + +from common.djangoapps.course_action_state.models import CourseRerunState +from common.djangoapps.course_modes.models import CourseMode +from lms.djangoapps.certificates.api import ( + create_course_certificate_generation_settings, + get_course_certificate_generation_settings, +) +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from xmodule.modulestore import EdxJSONEncoder, ModuleStoreEnum +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.exceptions import ItemNotFoundError + + +RERUN_COURSE_TASK_NAME = 'cms.djangoapps.contentstore.tasks.rerun_course' +CMS_CELERY_QUEUE = 'edx.cms.core.default' +CMS_CELERY_EXCHANGE = 'edx.cms.core' + +LOGGER = logging.getLogger(__name__) +RUN_PATTERN = re.compile(r"^(\d{4})_C([1-9]\d*)$", re.IGNORECASE) +DATE_FIELDS = frozenset(('start', 'end', 'enrollment_start', 'enrollment_end', 'certificate_available_date')) +BOOLEAN_FIELDS = frozenset(('self_paced', 'invitation_only')) +COURSE_FIELDS = DATE_FIELDS | BOOLEAN_FIELDS | {'catalog_visibility'} +WORKFLOW_BOOLEAN_FIELDS = frozenset(('shift_content_dates', 'hide_source')) +SETTINGS_FIELDS = COURSE_FIELDS | {'mode', 'certificate_mode'} | WORKFLOW_BOOLEAN_FIELDS + + +def _require_admin(user): + """Keep service calls as restricted as the HTTP entry points.""" + if not user.is_authenticated or not user.is_active or not user.is_staff: + raise PermissionDenied() + + +def validate_settings(payload, require_dates=False): + """Validate the small, explicit settings contract; omitted values stay unchanged. + + Date-only and timezone-less inputs use UTC. The page sends offsets explicitly. + Empty optional dates clear their field; start and end cannot be cleared. + """ + if not isinstance(payload, dict): + raise ValidationError('Настройки должны быть объектом JSON.') + unknown = set(payload) - SETTINGS_FIELDS + if unknown: + raise ValidationError(f'Неизвестные настройки: {", ".join(sorted(unknown))}.') + result = dict(payload) + for name in DATE_FIELDS & result.keys(): + value = result[name] + if value in (None, ''): + result[name] = None + else: + try: + parsed = value if isinstance(value, datetime) else parse_datetime(value) + except (TypeError, ValueError) as exc: + raise ValidationError(f'Некорректная дата: {name}.') from exc + if parsed is None: + raise ValidationError(f'Некорректная дата: {name}.') + result[name] = ( + parsed.replace(tzinfo=timezone.utc) if parsed.tzinfo is None else parsed.astimezone(timezone.utc) + ) + for name in ('start', 'end'): + if (require_dates or name in result) and result.get(name) is None: + raise ValidationError(f'Укажите дату {name}.') + for name in (BOOLEAN_FIELDS | WORKFLOW_BOOLEAN_FIELDS) & result.keys(): + if not isinstance(result[name], bool): + raise ValidationError(f'{name}: требуется true или false.') + choices = { + 'catalog_visibility': ('both', 'about', 'none'), + 'mode': ('copy', 'honor', 'audit'), + 'certificate_mode': ('copy', 'enabled', 'disabled'), + } + for name, allowed in choices.items(): + if name in result and result[name] not in allowed: + raise ValidationError(f'Недопустимое значение {name}.') + _validate_dates(result) + return result + + +def _validate_dates(values): + """Validate complete or partial course/enrollment calendars.""" + for start_name, end_name in (('start', 'end'), ('enrollment_start', 'enrollment_end')): + start, end = values.get(start_name), values.get(end_name) + if start and end and start >= end: + raise ValidationError(f'{end_name} должна быть позже {start_name}.') + if values.get('enrollment_end') and values.get('end') and values['enrollment_end'] > values['end']: + raise ValidationError('Окончание записи не может быть позже окончания курса.') + if values.get('enrollment_start') and values.get('end') and values['enrollment_start'] > values['end']: + raise ValidationError('Начало записи не может быть позже окончания курса.') + + +def _source_course(raw_key): + """Require a real, unversioned split-store course.""" + try: + key = raw_key if isinstance(raw_key, CourseKey) else CourseKey.from_string(raw_key) + except (InvalidKeyError, TypeError) as exc: + raise ValidationError('Некорректный идентификатор курса.') from exc + if (type(key) is not CourseLocator or key.deprecated # pylint: disable=unidiomatic-typecheck + or key.branch or key.version_guid): + raise ValidationError('Поддерживаются только курсы course-v1 без версии или ветки.') + course = modulestore().get_course(key, depth=0) + if course is None: + raise ValidationError('Курс не найден.') + return course + + +def _known_course_keys(): + """Include Mongo courses, SQL overviews and all outstanding run reservations.""" + keys = {course.id for course in modulestore().get_course_summaries()} + keys.update(CourseOverview.objects.values_list('id', flat=True)) + keys.update(CourseRerunState.objects.filter(action='rerun').values_list('course_key', flat=True)) + return keys + + +def next_course_key(source_key, year, known_keys): + """Choose the next numeric YYYY_Cn within this organization/course/year.""" + maximum = 0 + for key in known_keys: + if not isinstance(key, CourseKey): + key = CourseKey.from_string(key) + if key.org.casefold() != source_key.org.casefold() or key.course.casefold() != source_key.course.casefold(): + continue + match = RUN_PATTERN.fullmatch(key.run or '') + if match and int(match.group(1)) == year: + maximum = max(maximum, int(match.group(2))) + return CourseLocator(org=source_key.org, course=source_key.course, run=f'{year}_C{maximum + 1}') + + +def _generation_values(course, settings): + """Resolve the latest certificate settings, requiring an active template to enable.""" + original = get_course_certificate_generation_settings(course.id) + values = { + 'self_generation_enabled': original['self_generation_enabled'] if original else False, + 'language_specific_templates_enabled': original['language_specific_templates_enabled'] if original else False, + 'include_hours_of_effort': original['include_hours_of_effort'] if original else None, + } + policy = settings.get('certificate_mode', 'copy') + if policy != 'copy': + values['self_generation_enabled'] = policy == 'enabled' + if values['self_generation_enabled']: + certificates = (course.certificates or {}).get('certificates', []) + if not any(certificate.get('is_active') for certificate in certificates): + raise ValidationError('Сначала настройте и активируйте сертификат курса в Studio.') + return values + + +def _settings_for_course(course, settings, rerun=False): + """Validate resulting dates, including unchanged values on an existing course.""" + values = {name: getattr(course, name) for name in DATE_FIELDS} + if rerun: + values.update(enrollment_start=None, enrollment_end=None, certificate_available_date=None) + values.update(settings) + _validate_dates(values) + if rerun or settings.get('certificate_mode') == 'enabled': + _generation_values(course, settings) + + +def _result(source_key, course_key=None, state='failed', message='', state_id=None): + return { + 'source_course_id': str(source_key), + 'course_id': str(course_key) if course_key else None, + 'state': state, + 'message': message, + 'state_id': state_id, + } + + +def _error_message(exc): + if isinstance(exc, ValidationError): + return ' '.join(exc.messages) + return 'Операция не выполнена. Подробности записаны в журнал Studio.' + + +def _prepare_course(raw_key, settings, seen): + course = _source_course(raw_key) + family = (course.id.org.casefold(), course.id.course.casefold()) + if family in seen: + raise ValidationError('Выберите только один исходный запуск для одного курса.') + seen.add(family) + _settings_for_course(course, settings, rerun=True) + return course + + +def preview_course_reruns(user, course_keys, settings): + """Preflight each selection without reserving a key or starting a task.""" + _require_admin(user) + settings = validate_settings(settings, require_dates=True) + known, seen, results = _known_course_keys(), set(), [] + for raw_key in course_keys: + try: + course = _prepare_course(raw_key, settings, seen) + destination = next_course_key(course.id, settings['start'].year, known) + known.add(destination) + results.append(_result(course.id, destination, 'preview', 'Готов к перезапуску.')) + except Exception as exc: # pylint: disable=broad-except + LOGGER.exception('Course admin preflight failed for %s', raw_key) + results.append(_result(raw_key, message=_error_message(exc))) + return results + + +def _reserve_rerun(course, user, year, known, expected=None): + """The SQL unique constraint arbitrates simultaneous requests for the next run. + +Never use the existing manager's initiated()/get_or_create(): it overwrites a +competing worker's reservation. Each collision gets a separate savepoint. +""" + if expected: + try: + expected = CourseKey.from_string(expected) + except (InvalidKeyError, TypeError) as exc: + raise ValidationError('Некорректный идентификатор из предварительного просмотра.') from exc + match = RUN_PATTERN.fullmatch(expected.run or '') + if (type(expected) is not CourseLocator # pylint: disable=unidiomatic-typecheck + or expected.branch or expected.version_guid + or expected.org != course.id.org or expected.course != course.id.course + or not match or int(match.group(1)) != year): + raise ValidationError('Идентификатор запуска не соответствует выбранному курсу и году.') + previous = CourseRerunState.objects.filter(course_key=expected, action='rerun').first() + if previous: + if previous.source_course_key == course.id and previous.created_user_id == user.id: + return previous, False + raise ValidationError('Этот запуск уже зарезервирован. Обновите предварительный просмотр.') + if expected != next_course_key(course.id, year, known): + raise ValidationError('Список запусков изменился. Обновите предварительный просмотр.') + for _ in range(1000): + destination = expected or next_course_key(course.id, year, known) + known.add(destination) + if modulestore().has_course(destination, ignore_case=True): + if expected: + raise ValidationError('Курс уже существует. Обновите предварительный просмотр.') + continue + try: + with transaction.atomic(): + reservation = CourseRerunState.objects.create( + course_key=destination, + action='rerun', + state='in_progress', + source_course_key=course.id, + created_user=user, + updated_user=user, + display_name=course.display_name or str(course.id), + should_display=True, + message='', + ) + return reservation, True + except IntegrityError: + # Another request reserved this run between the read and insert. + if expected: + previous = CourseRerunState.objects.filter(course_key=expected, action='rerun').first() + if previous and previous.source_course_key == course.id and previous.created_user_id == user.id: + return previous, False + raise ValidationError('Этот запуск уже зарезервирован. Обновите предварительный просмотр.') from None + continue + raise ValidationError('Не удалось зарезервировать запуск. Повторите запрос.') + + +def _send_rerun_task(source, destination, user_id, fields, settings): + """Send the CMS-only cloning task without importing CMS Django apps in LMS.""" + current_app.send_task( + RERUN_COURSE_TASK_NAME, + args=( + str(source), + str(destination), + user_id, + json.dumps(fields, cls=EdxJSONEncoder), + ), + kwargs={ + 'admin_settings': json.loads(json.dumps(settings, cls=EdxJSONEncoder)), + }, + queue=CMS_CELERY_QUEUE, + exchange=CMS_CELERY_EXCHANGE, + routing_key=CMS_CELERY_QUEUE, + ) + + +def queue_course_reruns(user, course_keys, settings, expected_course_ids=None): + """Reserve distinct destinations and dispatch one existing Studio task per course.""" + _require_admin(user) + settings = validate_settings(settings, require_dates=True) + if expected_course_ids is not None: + if isinstance(expected_course_ids, list) and len(expected_course_ids) == len(course_keys): + expected_course_ids = dict(zip(map(str, course_keys), expected_course_ids)) + if not isinstance(expected_course_ids, dict) or set(expected_course_ids) != set(map(str, course_keys)): + raise ValidationError('Предварительный просмотр должен включать все выбранные курсы.') + if not all(isinstance(value, str) and value for value in expected_course_ids.values()): + raise ValidationError('Предварительный просмотр содержит некорректные идентификаторы.') + known, seen, results = _known_course_keys(), set(), [] + for raw_key in course_keys: + reservation = None + try: + course = _prepare_course(raw_key, settings, seen) + reservation, created = _reserve_rerun( + course, user, settings['start'].year, known, + expected=expected_course_ids[str(raw_key)] if expected_course_ids is not None else None, + ) + destination = reservation.course_key + if not created: + results.append(_result( + course.id, destination, reservation.state, + 'Этот запрос уже принят; новый запуск не создан.', reservation.pk, + )) + continue + fields = {name: value for name, value in settings.items() if name in COURSE_FIELDS} + fields.update( + display_name=course.display_name, + wiki_slug=f'{destination.org}.{destination.course}.{destination.run}', + advertised_start=None, + video_upload_pipeline={}, + ) + for name in ('enrollment_start', 'enrollment_end', 'certificate_available_date'): + fields.setdefault(name, None) + if settings.get('certificate_mode') == 'enabled': + fields['cert_html_view_enabled'] = True + result = _result(course.id, destination, 'in_progress', 'Задача поставлена в очередь.', reservation.pk) + # Enqueue only once the unique reservation is committed. With autocommit + # this executes immediately; it also remains safe under ATOMIC_REQUESTS. + + def dispatch(state=reservation, source=course.id, overrides=fields, row=result): + try: + _send_rerun_task(source, state.course_key, user.id, overrides, settings) + except Exception: # pylint: disable=broad-except + LOGGER.exception('Could not enqueue admin rerun for %s', state.course_key) + CourseRerunState.objects.failed(course_key=state.course_key) + row.update(state='failed', message='Не удалось отправить задачу в очередь. Проверьте Celery.') + transaction.on_commit(dispatch) + results.append(result) + except Exception as exc: # pylint: disable=broad-except + LOGGER.exception('Course admin rerun failed for %s', raw_key) + if reservation: + CourseRerunState.objects.failed(course_key=reservation.course_key) + results.append(_result( + raw_key, reservation.course_key if reservation else None, + message=_error_message(exc), state_id=reservation.pk if reservation else None, + )) + return results + + +def _ensure_mode(course_key, slug): + """Add a free mode without removing or modifying existing paid tracks.""" + CourseMode.objects.get_or_create( + course_id=course_key, mode_slug=slug, currency='usd', + defaults={'mode_display_name': 'Honor' if slug == 'honor' else 'Audit', 'min_price': 0}, + ) + + +def configure_rerun(source_key, destination_key, settings): + """Copy only course-level SQL settings after the Studio clone has completed. + +Caller wraps this and its final success state in the same SQL transaction. +The clone already carries grading, content, active certificate design and other +Studio course fields. Ecommerce identifiers belong to the source product and +must be provisioned separately for a new course run. +""" + settings = validate_settings(settings, require_dates=True) + source = _source_course(source_key) + generation_values = _generation_values(source, settings) + CourseOverview.get_from_id(destination_key) + policy = settings.get('mode', 'copy') + if policy == 'copy': + modes = list(CourseMode.objects.filter(course_id=source_key)) + for mode in modes: + values = { + field.name: getattr(mode, field.name) + for field in CourseMode._meta.concrete_fields + if field.name not in ('id', 'course', 'mode_slug', 'currency') + } + delta = settings['start'] - source.start if source.start else None + if delta and values['_expiration_datetime']: + values['_expiration_datetime'] += delta + if delta and values['expiration_date']: + values['expiration_date'] += delta + for name in ('sku', 'android_sku', 'ios_sku', 'bulk_sku'): + values[name] = None + CourseMode.objects.update_or_create( + course_id=destination_key, mode_slug=mode.mode_slug, currency=mode.currency, defaults=values, + ) + if not modes: + default = CourseMode.DEFAULT_MODE + CourseMode.objects.get_or_create( + course_id=destination_key, mode_slug=default.slug, currency=default.currency, + defaults={ + 'mode_display_name': default.name, 'min_price': default.min_price, + 'suggested_prices': default.suggested_prices, 'description': default.description, + }, + ) + else: + _ensure_mode(destination_key, policy) + create_course_certificate_generation_settings(destination_key, generation_values) + + +def hide_source_course(source_key, user_id): + """Hide a successfully replaced source run from course discovery.""" + course = _source_course(source_key) + if course.catalog_visibility == 'none': + return + previous_visibility = course.catalog_visibility + try: + course.catalog_visibility = 'none' + modulestore().update_item(course, user_id) + CourseOverview.load_from_module_store(course.id) + except Exception: + LOGGER.exception('Could not hide source course %s; restoring its visibility', source_key) + restored = _source_course(source_key) + restored.catalog_visibility = previous_visibility + modulestore().update_item(restored, user_id) + CourseOverview.load_from_module_store(restored.id) + raise + + +def shift_rerun_content_dates(source_key, destination_key, user_id, settings): + """Shift explicit section/component dates in each branch without publishing drafts. + +Use the split store's branch-local update primitive intentionally: the normal +Studio update auto-publishes chapters/sequentials, replacing published content +with draft content. The two existing branches must remain independent here. +Advanced schedules embedded in third-party XBlock JSON are not rewritten. +""" + if not settings.get('shift_content_dates', True): + return + from xmodule.modulestore.split_mongo.split import SplitMongoModuleStore # pylint: disable=import-outside-toplevel + + settings = validate_settings(settings, require_dates=True) + source = _source_course(source_key) + if not source.start: + return + delta = settings['start'] - source.start + if not delta: + return + store = modulestore()._get_modulestore_for_courselike(destination_key) # pylint: disable=protected-access + with store.bulk_operations(destination_key): + published_changed = False + for branch in (ModuleStoreEnum.BranchName.published, ModuleStoreEnum.BranchName.draft): + try: + blocks = store.get_items(destination_key.for_branch(branch)) + except ItemNotFoundError: + # A draft-only source may have no published branch. + continue + for block in blocks: + if block.category == 'course': + continue + changed = False + for name in ('start', 'due'): + field = block.fields.get(name) + if field and field.is_set_on(block) and getattr(block, name) is not None: + setattr(block, name, getattr(block, name) + delta) + changed = True + if changed: + # Drop the snapshot version so multiple updates advance this + # branch's current head instead of writing detached versions. + block.location = block.location.version_agnostic() + SplitMongoModuleStore.update_item(store, block, user_id) + published_changed = published_changed or branch == ModuleStoreEnum.BranchName.published + if published_changed: + store._flag_publish_event(destination_key) # pylint: disable=protected-access + + +def save_course_settings(user, course_keys, settings): + """Apply selected fields to each existing course; preserve unselected settings.""" + _require_admin(user) + settings = validate_settings(settings) + results, seen = [], set() + for raw_key in course_keys: + previous_fields = None + try: + course = _source_course(raw_key) + if course.id in seen: + raise ValidationError('Курс указан повторно.') + seen.add(course.id) + _settings_for_course(course, settings) + with transaction.atomic(): + CourseOverview.get_from_id(course.id) + # Serialize SQL configuration updates for this existing course. + CourseOverview.objects.select_for_update().get(id=course.id) + course = _source_course(course.id) + _settings_for_course(course, settings) + if settings.get('mode', 'copy') != 'copy': + _ensure_mode(course.id, settings['mode']) + generation_values = None + if settings.get('certificate_mode', 'copy') != 'copy': + generation_values = _generation_values(course, settings) + touched = set(settings) & COURSE_FIELDS + if settings.get('certificate_mode') == 'enabled': + touched.add('cert_html_view_enabled') + previous_fields = { + name: (course.fields[name].is_set_on(course), deepcopy(getattr(course, name))) + for name in touched + } + for name, value in settings.items(): + if name in COURSE_FIELDS: + setattr(course, name, value) + if settings.get('certificate_mode') == 'enabled': + course.cert_html_view_enabled = True + modulestore().update_item(course, user.id) + CourseOverview.load_from_module_store(course.id) + if generation_values is not None: + create_course_certificate_generation_settings(course.id, generation_values) + results.append(_result(course.id, course.id, 'saved', 'Настройки сохранены.')) + except Exception as exc: # pylint: disable=broad-except + LOGGER.exception('Course admin settings failed for %s', raw_key) + message = _error_message(exc) + if previous_fields: + # SQL rolled back above, but Mongo does not participate in its + # transaction. Restore precisely the touched fields on failure. + try: + with transaction.atomic(): + CourseOverview.objects.select_for_update().get(id=course.id) + restored = _source_course(course.id) + for name, (was_set, value) in previous_fields.items(): + if was_set: + setattr(restored, name, value) + else: + delattr(restored, name) + modulestore().update_item(restored, user.id) + CourseOverview.load_from_module_store(restored.id) + except Exception: # pylint: disable=broad-except + LOGGER.exception('Could not restore course settings for %s', raw_key) + message += ' Часть настроек могла сохраниться; проверьте курс в Studio.' + results.append(_result(raw_key, raw_key, message=message)) + return results diff --git a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py index 6c2660428e66..436e9be6fa61 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py +++ b/cms/djangoapps/contentstore/rest_api/v1/serializers/course_details.py @@ -34,6 +34,7 @@ class CourseDetailsSerializer(serializers.Serializer): description = serializers.CharField(allow_blank=True) duration = serializers.CharField(allow_blank=True) effort = serializers.CharField(allow_null=True, allow_blank=True) + # complexity = serializers.CharField(allow_null=True, allow_blank=True, required=False) end_date = serializers.DateTimeField(allow_null=True) enrollment_end = serializers.DateTimeField(allow_null=True) enrollment_start = serializers.DateTimeField(allow_null=True) diff --git a/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py b/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py index d5ccf3c6165e..e6ad4b95979f 100644 --- a/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py +++ b/cms/djangoapps/contentstore/rest_api/v1/views/course_details.py @@ -151,5 +151,13 @@ def put(self, request: Request, course_id: str): except ValidationError as err: return JsonResponseBadRequest({"error": err.message}) + from openedx.core.djangoapps.content.course_overviews.models import CourseOverview + + print("REQUEST DATA:", course_id, request.data) + complexity = request.data.get("complexity") + + if complexity: + CourseOverview.objects.filter(id=course_key).update(complexity=complexity) + serializer = CourseDetailsSerializer(updated_data) return Response(serializer.data) diff --git a/cms/djangoapps/contentstore/tasks.py b/cms/djangoapps/contentstore/tasks.py index b6cb2af53d56..d43281fe9eed 100644 --- a/cms/djangoapps/contentstore/tasks.py +++ b/cms/djangoapps/contentstore/tasks.py @@ -22,6 +22,7 @@ from django.contrib.auth import get_user_model from django.core.exceptions import SuspiciousOperation from django.core.files import File +from django.db import transaction from django.test import RequestFactory from django.utils.text import get_valid_filename from edx_django_utils.monitoring import ( @@ -131,12 +132,72 @@ def clone_instance(instance, field_values): return instance +def copy_custom_course_overview_fields(source_course_key, destination_course_key, raise_on_error=False): + """ + Copy custom CourseOverview fields from source course to rerun course. + """ + from openedx.core.djangoapps.content.course_overviews.models import CourseOverview + + try: + source_overview = CourseOverview.objects.get(id=source_course_key) + + try: + destination_overview = CourseOverview.objects.get(id=destination_course_key) + except CourseOverview.DoesNotExist: + destination_overview = CourseOverview.get_from_id(destination_course_key) + + destination_overview.faculty = source_overview.faculty + destination_overview.directions = source_overview.directions + destination_overview.complexity = source_overview.complexity + + destination_overview.save(update_fields=[ + "faculty", + "directions", + "complexity", + ]) + + LOGGER.info( + "my-log: copied custom overview fields during course rerun: %s -> %s, faculty=%s, directions=%s, complexity=%s", + source_course_key, + destination_course_key, + source_overview.faculty, + source_overview.directions, + source_overview.complexity, + ) + + except Exception: + LOGGER.exception( + "my-log: failed to copy custom overview fields during course rerun: %s -> %s", + source_course_key, + destination_course_key, + ) + if raise_on_error: + raise + + @shared_task @set_code_owner_attribute -def rerun_course(source_course_key_string, destination_course_key_string, user_id, fields=None): +def rerun_course(source_course_key_string, destination_course_key_string, user_id, fields=None, admin_settings=None): """ Reruns a course in a new celery task. """ + if admin_settings is not None: + # Celery may redeliver a task. Hold the destination reservation lock so + # concurrent deliveries cannot clone or overwrite one another's status. + with transaction.atomic(): + state = CourseRerunState.objects.select_for_update().get( + course_key=CourseKey.from_string(destination_course_key_string), action='rerun', + ) + if state.state != 'in_progress': + return state.state + return _rerun_course( + source_course_key_string, destination_course_key_string, user_id, fields, admin_settings, + ) + return _rerun_course(source_course_key_string, destination_course_key_string, user_id, fields) + + +def _rerun_course(source_course_key_string, destination_course_key_string, user_id, fields=None, admin_settings=None): + """Implementation shared by native Studio reruns and reserved admin reruns.""" # import here, at top level this import prevents the celery workers from starting up correctly from edxval.api import copy_course_videos @@ -152,34 +213,60 @@ def rerun_course(source_course_key_string, destination_course_key_string, user_i with store.default_store('split'): store.clone_course(source_course_key, destination_course_key, user_id, fields=fields) - update_unit_discussion_state_from_discussion_blocks(destination_course_key, user_id) + if admin_settings is not None: + from cms.djangoapps.contentstore.course_admin import shift_rerun_content_dates + shift_rerun_content_dates(source_course_key, destination_course_key, user_id, admin_settings) - # set initial permissions for the user to access the course. - initialize_permissions(destination_course_key, User.objects.get(id=user_id)) + copy_custom_course_overview_fields( + source_course_key, + destination_course_key, + raise_on_error=admin_settings is not None, + ) - # update state: Succeeded - CourseRerunState.objects.succeeded(course_key=destination_course_key) + update_unit_discussion_state_from_discussion_blocks(destination_course_key, user_id) # call edxval to attach videos to the rerun copy_course_videos(source_course_key, destination_course_key) - # Copy OrganizationCourse - organization_course = OrganizationCourse.objects.filter(course_id=source_course_key_string).first() - - if organization_course: - clone_instance(organization_course, {'course_id': destination_course_key_string}) - - # Copy RestrictedCourse - restricted_course = RestrictedCourse.objects.filter(course_key=source_course_key).first() - - if restricted_course: - country_access_rules = CountryAccessRule.objects.filter(restricted_course=restricted_course) - new_restricted_course = clone_instance(restricted_course, {'course_key': destination_course_key}) - for country_access_rule in country_access_rules: - clone_instance(country_access_rule, {'restricted_course': new_restricted_course}) - - org_data = ensure_organization(source_course_key.org) - add_organization_course(org_data, destination_course_key) + # Success includes every copy and the admin workflow's SQL configuration. + # Keep SQL copies in one transaction, and apply admin configuration after + # modulestore signals so pacing handlers cannot overwrite certificate settings. + with transaction.atomic(): + # Set initial permissions for the user to access the course. + creator = User.objects.get(id=user_id) + if admin_settings is not None: + # The private bulk workflow reserves directly instead of invoking the + # single-course HTTP helper that normally grants these roles. + CourseInstructorRole(destination_course_key).add_users(creator) + CourseStaffRole(destination_course_key).add_users(creator) + initialize_permissions(destination_course_key, creator) + + # Copy OrganizationCourse + organization_course = OrganizationCourse.objects.filter(course_id=source_course_key_string).first() + if organization_course: + clone_instance(organization_course, {'course_id': destination_course_key_string}) + + # Copy RestrictedCourse + restricted_course = RestrictedCourse.objects.filter(course_key=source_course_key).first() + if restricted_course: + country_access_rules = CountryAccessRule.objects.filter(restricted_course=restricted_course) + new_restricted_course = clone_instance(restricted_course, {'course_key': destination_course_key}) + for country_access_rule in country_access_rules: + clone_instance(country_access_rule, {'restricted_course': new_restricted_course}) + + org_data = ensure_organization(source_course_key.org) + add_organization_course(org_data, destination_course_key) + if admin_settings is not None: + from cms.djangoapps.contentstore.course_admin import configure_rerun, hide_source_course + configure_rerun(source_course_key, destination_course_key, admin_settings) + # Split Mongo cloning deliberately skips automatic publishing. Build + # the SQL outline before reporting success so the Learning MFE can + # open the copied course immediately. + if key_supports_outlines(destination_course_key): + update_outline_from_modulestore(destination_course_key) + if admin_settings.get('hide_source'): + hide_source_course(source_course_key, user_id) + CourseRerunState.objects.succeeded(course_key=destination_course_key) return "succeeded" except DuplicateCourseError: diff --git a/cms/djangoapps/contentstore/tests/test_course_admin.py b/cms/djangoapps/contentstore/tests/test_course_admin.py new file mode 100644 index 000000000000..8bbc1a1543a7 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_course_admin.py @@ -0,0 +1,160 @@ +"""Unit tests for the private course administration service.""" + +from contextlib import nullcontext +from datetime import datetime, timezone +from types import SimpleNamespace +from unittest.mock import Mock, patch + +from django.core.exceptions import PermissionDenied, ValidationError +from django.test import SimpleTestCase +from opaque_keys.edx.locator import CourseLocator + +from cms.djangoapps.contentstore import course_admin + + +SOURCE = CourseLocator(org='FBB', course='BNJjTSOB', run='2026_C1') + + +class CourseAdminSettingsTests(SimpleTestCase): + """The bulk API accepts only explicit, internally consistent settings.""" + + def test_dates_are_normalized_to_utc(self): + result = course_admin.validate_settings({ + 'start': '2027-01-01T09:00:00+05:00', + 'end': '2027-05-31T09:00:00+05:00', + 'self_paced': True, + 'catalog_visibility': 'about', + 'hide_source': True, + }, require_dates=True) + + self.assertEqual(result['start'], datetime(2027, 1, 1, 4, tzinfo=timezone.utc)) + self.assertEqual(result['end'], datetime(2027, 5, 31, 4, tzinfo=timezone.utc)) + self.assertTrue(result['self_paced']) + self.assertTrue(result['hide_source']) + + def test_rejects_unknown_settings_invalid_choices_and_non_boolean_values(self): + invalid_payloads = ( + {'unexpected': 'value'}, + {'catalog_visibility': 'public'}, + {'mode': 'verified'}, + {'certificate_mode': 'yes'}, + {'self_paced': 'true'}, + {'hide_source': 'true'}, + ) + for payload in invalid_payloads: + with self.subTest(payload=payload), self.assertRaises(ValidationError): + course_admin.validate_settings(payload) + + def test_rejects_inconsistent_course_and_enrollment_periods(self): + invalid_payloads = ( + {'start': '2027-02-01T00:00:00Z', 'end': '2027-01-01T00:00:00Z'}, + {'enrollment_start': '2027-03-01T00:00:00Z', + 'enrollment_end': '2027-02-01T00:00:00Z'}, + {'end': '2027-03-01T00:00:00Z', 'enrollment_end': '2027-04-01T00:00:00Z'}, + ) + for payload in invalid_payloads: + with self.subTest(payload=payload), self.assertRaises(ValidationError): + course_admin.validate_settings(payload) + + def test_service_rejects_non_admin_and_inactive_staff(self): + for user in ( + SimpleNamespace(is_authenticated=False, is_active=True, is_staff=True), + SimpleNamespace(is_authenticated=True, is_active=True, is_staff=False), + SimpleNamespace(is_authenticated=True, is_active=False, is_staff=True), + ): + with self.subTest(user=user), self.assertRaises(PermissionDenied): + course_admin.preview_course_reruns(user, [], { + 'start': '2027-01-01T00:00:00Z', 'end': '2027-02-01T00:00:00Z', + }) + + +class CourseAdminRunTests(SimpleTestCase): + """Automatic run numbers work with numeric C suffixes and legacy sources.""" + + def test_rerun_is_sent_to_cms_worker_without_importing_the_cms_task(self): + settings = { + 'start': datetime(2027, 1, 1, tzinfo=timezone.utc), + 'end': datetime(2027, 5, 31, tzinfo=timezone.utc), + } + fields = {'display_name': 'Biology'} + + with patch.object(course_admin, 'current_app') as celery_app: + course_admin._send_rerun_task(SOURCE, SOURCE, 7, fields, settings) + + celery_app.send_task.assert_called_once_with( + course_admin.RERUN_COURSE_TASK_NAME, + args=(str(SOURCE), str(SOURCE), 7, '{"display_name": "Biology"}'), + kwargs={ + 'admin_settings': { + 'start': '2027-01-01T00:00:00+00:00', + 'end': '2027-05-31T00:00:00+00:00', + }, + }, + queue=course_admin.CMS_CELERY_QUEUE, + exchange=course_admin.CMS_CELERY_EXCHANGE, + routing_key=course_admin.CMS_CELERY_QUEUE, + ) + + def test_next_run_uses_highest_numeric_suffix_for_year_and_family(self): + known = { + CourseLocator(org='FBB', course='BNJjTSOB', run='2027_C2'), + CourseLocator(org='fbb', course='bnjjtsob', run='2027_C10'), + CourseLocator(org='FBB', course='BNJjTSOB', run='2026_C99'), + CourseLocator(org='FBB', course='OTHER', run='2027_C50'), + CourseLocator(org='FBB', course='BNJjTSOB', run='2021-2022_C1'), + } + + result = course_admin.next_course_key(SOURCE, 2027, known) + + self.assertEqual(str(result), 'course-v1:FBB+BNJjTSOB+2027_C11') + + def test_certificate_generation_requires_an_active_studio_certificate(self): + source = SimpleNamespace(id=SOURCE, certificates={'certificates': [{'is_active': False}]}) + current = { + 'self_generation_enabled': True, + 'language_specific_templates_enabled': False, + 'include_hours_of_effort': None, + } + with patch.object(course_admin, 'get_course_certificate_generation_settings', return_value=current): + with self.assertRaises(ValidationError): + course_admin._generation_values(source, {'certificate_mode': 'copy'}) + + source.certificates['certificates'][0]['is_active'] = True + result = course_admin._generation_values(source, {'certificate_mode': 'copy'}) + + self.assertTrue(result['self_generation_enabled']) + + def test_shift_content_dates_updates_published_and_draft_branches_independently(self): + source_start = datetime(2026, 1, 1, tzinfo=timezone.utc) + published = self._block(datetime(2026, 2, 1, tzinfo=timezone.utc)) + draft = self._block(datetime(2026, 2, 3, tzinfo=timezone.utc)) + store = Mock() + store.bulk_operations.return_value = nullcontext() + store.get_items.side_effect = ([published], [draft]) + mixed_store = Mock() + mixed_store._get_modulestore_for_courselike.return_value = store + settings = { + 'start': '2027-01-01T00:00:00Z', + 'end': '2027-05-31T00:00:00Z', + 'shift_content_dates': True, + } + + with patch.object(course_admin, '_source_course', return_value=SimpleNamespace(start=source_start)), \ + patch.object(course_admin, 'modulestore', return_value=mixed_store), \ + patch('xmodule.modulestore.split_mongo.split.SplitMongoModuleStore.update_item') as update: + course_admin.shift_rerun_content_dates(SOURCE, SOURCE, 7, settings) + + self.assertEqual(published.start, datetime(2027, 2, 1, tzinfo=timezone.utc)) + self.assertEqual(draft.start, datetime(2027, 2, 3, tzinfo=timezone.utc)) + self.assertEqual(update.call_count, 2) + store._flag_publish_event.assert_called_once_with(SOURCE) + + @staticmethod + def _block(start): + date_field = Mock() + date_field.is_set_on.return_value = True + location = Mock() + location.version_agnostic.return_value = location + return SimpleNamespace( + category='chapter', fields={'start': date_field}, start=start, location=location, + ) diff --git a/cms/djangoapps/contentstore/tests/test_tasks.py b/cms/djangoapps/contentstore/tests/test_tasks.py index 103e24deb821..05341405d732 100644 --- a/cms/djangoapps/contentstore/tests/test_tasks.py +++ b/cms/djangoapps/contentstore/tests/test_tasks.py @@ -189,6 +189,29 @@ def test_success(self): country=restricted_country ) + @mock.patch('cms.djangoapps.contentstore.course_admin.shift_rerun_content_dates') + @mock.patch('cms.djangoapps.contentstore.course_admin.configure_rerun') + @mock.patch('cms.djangoapps.contentstore.course_admin.hide_source_course') + @mock.patch('cms.djangoapps.contentstore.tasks.update_outline_from_modulestore') + def test_admin_rerun_builds_outline_and_hides_source( + self, update_outline, hide_source, _configure, _shift_dates, + ): + """An admin rerun is usable in Learning and can retire its source run.""" + old_course_key = self.course.id + new_course_key = CourseLocator(org=old_course_key.org, course=old_course_key.course, run='admin-rerun') + CourseRerunState.objects.initiated(old_course_key, new_course_key, self.user, 'Admin Re-run') + + result = rerun_course( + str(old_course_key), + str(new_course_key), + self.user.id, + admin_settings={'hide_source': True}, + ) + + self.assertEqual(result, 'succeeded') + update_outline.assert_called_once_with(new_course_key) + hide_source.assert_called_once_with(old_course_key, self.user.id) + @override_settings(CONTENTSTORE=TEST_DATA_CONTENTSTORE) class RegisterExamsTaskTestCase(CourseTestCase): # pylint: disable=missing-class-docstring diff --git a/cms/djangoapps/contentstore/views/course_admin.py b/cms/djangoapps/contentstore/views/course_admin.py new file mode 100644 index 000000000000..03b3724fe59d --- /dev/null +++ b/cms/djangoapps/contentstore/views/course_admin.py @@ -0,0 +1,285 @@ +"""Private Studio console for global administrators managing course runs.""" + +import json +import logging +import re +from datetime import timezone +from functools import wraps + +from django.conf import settings +from django.core import signing +from django.core.exceptions import ValidationError +from django.core.paginator import Paginator +from django.http import JsonResponse +from django.middleware.csrf import get_token +from django.urls import reverse +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect, ensure_csrf_cookie +from django.views.decorators.http import require_GET, require_POST +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.locator import CourseLocator + +from cms.djangoapps.contentstore import course_admin as service +from cms.djangoapps.contentstore.utils import get_schedule_details_url +from common.djangoapps.course_action_state.models import CourseRerunState +from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.edxmako.shortcuts import render_to_response +from common.djangoapps.student.roles import GlobalStaff +from lms.djangoapps.certificates.api import get_self_generation_enabled_for_courses +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview +from xmodule.modulestore.django import modulestore + + +LOG = logging.getLogger(__name__) +PREVIEW_SALT = 'studio.course-admin.preview.v1' +RUN_PATTERN = re.compile(r'^(\d{4})_C([1-9]\d*)$', re.IGNORECASE) + + +def admin_required(view): + """Check every endpoint, including reads, independently of navigation.""" + @wraps(view) + def wrapped(request, *args, **kwargs): + if not (request.user.is_authenticated and request.user.is_active and GlobalStaff().has_user(request.user)): + return JsonResponse({'error': 'Доступ разрешён только администраторам платформы.'}, status=403) + return view(request, *args, **kwargs) + return wrapped + + +def parse_filters(values): + """Accept bounded, literal filters from either a query string or JSON.""" + if not isinstance(values, dict) and not hasattr(values, 'getlist'): + raise ValueError('Некорректные фильтры.') + result = {} + for field in ('q', 'org', 'run', 'year'): + value = values.get(field, '') + if not isinstance(value, str) or len(value) > 255: + raise ValueError('Некорректное значение фильтра.') + result[field] = value.strip() + if result['year'] and not re.fullmatch(r'[1-9]\d{3}', result['year']): + raise ValueError('Год должен состоять из четырёх цифр.') + latest = values.get('latest', True) + if latest not in (True, False, '1', '0', 'true', 'false', 'on', ''): + raise ValueError('Некорректный фильтр последнего запуска.') + result['latest'] = latest in (True, '1', 'true', 'on') + return result + + +def filter_courses(courses, filters): + """Filter arbitrary legacy runs, then select the latest matching family run.""" + selected = [] + for course in courses: + key = course['key'] + if filters['org'] and filters['org'].casefold() != key.org.casefold(): + continue + if filters['run'] and filters['run'].casefold() != key.run.casefold(): + continue + if filters['year'] and not key.run.startswith(filters['year']): + continue + searchable = f"{key} {course['name']} {course.get('faculty') or ''}".casefold() + if filters['q'] and filters['q'].casefold() not in searchable: + continue + selected.append(course) + if filters['latest']: + families = {} + for course in selected: + key = course['key'] + family = (key.org.casefold(), key.course.casefold()) + previous = families.get(family) + if previous is None or _run_order(course) > _run_order(previous): + families[family] = course + selected = list(families.values()) + return sorted(selected, key=lambda course: (course['name'].casefold(), str(course['key']))) + + +def _run_order(course): + """Compare C10 numerically; legacy runs fall back to their course start.""" + match = RUN_PATTERN.fullmatch(course['key'].run) + start = course.get('start') + year = int(match[1]) if match else (start.year if start else 0) + sequence = int(match[2]) if match else 0 + return year, sequence, str(start or ''), course['key'].run + + +def _courses(): + """Use Studio's inventory, including drafts absent from the SQL overview cache.""" + overviews = {str(course.id): course for course in CourseOverview.objects.all()} + courses = [] + for summary in modulestore().get_course_summaries(): + key = summary.id + # Native reruns require split course keys; libraries/CCX are not courses to clone here. + if type(key) is not CourseLocator or key.deprecated: # pylint: disable=unidiomatic-typecheck + continue + overview = overviews.get(str(key)) + courses.append({ + 'key': key, + 'name': summary.display_name or str(key), + 'start': getattr(overview, 'start', None), + 'end': getattr(overview, 'end', None), + 'enrollment_start': getattr(overview, 'enrollment_start', None), + 'enrollment_end': getattr(overview, 'enrollment_end', None), + 'visibility': getattr(overview, 'catalog_visibility', None), + 'faculty': getattr(overview, 'faculty', ''), + 'self_paced': getattr(overview, 'self_paced', False), + 'invitation_only': getattr(overview, 'invitation_only', False), + 'certificate_available_date': getattr(overview, 'certificate_available_date', None), + }) + return courses + + +def _display_course(course, modes, generation): + row = dict(course) + key = row.pop('key') + row.update(id=str(key), org=key.org, run=key.run) + row['mode_labels'] = ', '.join(modes.get(str(key), [])) or f'{CourseMode.DEFAULT_MODE_SLUG} (по умолчанию)' + row['certificate_generation'] = 'Включена' if generation.get(str(key)) else 'Выключена' + row['self_paced_label'] = 'В своём темпе' if row.pop('self_paced') else 'По расписанию преподавателя' + row['invitation_only_label'] = 'Запись только по приглашению' if row.pop('invitation_only') else 'Открытая запись' + for field in ('start', 'end', 'enrollment_start', 'enrollment_end', 'certificate_available_date'): + value = row[field] + row[field] = value.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M') if value else '' + row['details_url'] = get_schedule_details_url(key) or reverse( + 'settings_handler', kwargs={'course_key_string': str(key)}, + ) + return row + + +def _state_result(state): + return { + 'source_course_id': str(state.source_course_key), + 'course_id': str(state.course_key), + 'state': state.state, + 'message': ('Перезапуск завершён.' if state.state == 'succeeded' else + 'Ошибка перезапуска. Подробности доступны в журнале CMS worker.' if state.state == 'failed' else + 'Курс создаётся в фоновой задаче.'), + } + + +@never_cache +@admin_required +@ensure_csrf_cookie +@require_GET +def course_admin_page(request): + """Render a direct-link-only management console, without adding any menu entry.""" + try: + filters = parse_filters(request.GET) + except ValueError as exc: + return JsonResponse({'error': str(exc)}, status=400) + courses = filter_courses(_courses(), filters) + page = Paginator(courses, 50).get_page(request.GET.get('page', 1)) + page_keys = [course['key'] for course in page.object_list] + modes = {} + for mode in CourseMode.objects.filter(course_id__in=page_keys).order_by('mode_slug', 'currency'): + modes.setdefault(str(mode.course_id), []).append(f'{mode.mode_slug} ({mode.currency})') + generation = get_self_generation_enabled_for_courses(page_keys) + recent = CourseRerunState.objects.find_all(created_user=request.user).order_by('-created_time')[:50] + return render_to_response('course-admin.html', { + 'courses': [_display_course(course, modes, generation) for course in page.object_list], + 'course_filters': filters, + 'page': page.number, + 'num_pages': page.paginator.num_pages, + 'total': page.paginator.count, + 'endpoint_url': reverse('course_admin_action'), + 'status_url': reverse('course_admin_status'), + 'initial_results': [_state_result(state) for state in recent], + 'timezone_label': 'UTC (Казахстан: UTC+5)', + 'csrf_token': get_token(request), + }) + + +def _selected_keys(payload): + all_matching = payload.get('all_matching', False) + if not isinstance(all_matching, bool): + raise ValueError('Некорректный способ выбора курсов.') + if all_matching: + keys = [course['key'] for course in filter_courses(_courses(), parse_filters(payload.get('filters', {})))] + else: + identifiers = payload.get('course_ids', []) + if not isinstance(identifiers, list) or not all(isinstance(value, str) for value in identifiers): + raise ValueError('Передайте список идентификаторов курсов.') + keys = [] + for identifier in dict.fromkeys(identifiers): + key = CourseKey.from_string(identifier) + if (type(key) is not CourseLocator or key.deprecated or key.branch # pylint: disable=unidiomatic-typecheck + or key.version_guid): + raise ValueError('Поддерживаются только обычные курсы Studio с идентификатором course-v1.') + keys.append(key) + limit = getattr(settings, 'COURSE_ADMIN_MAX_BATCH_SIZE', 1000) + if not keys or len(keys) > limit: + raise ValueError(f'Выберите от 1 до {limit} курсов. Уточните фильтры для большой выборки.') + return keys + + +@never_cache +@admin_required +@require_POST +@csrf_protect +def course_admin_action(request): + """Validate a complete batch before dispatch; report operational failures per course.""" + try: + payload = json.loads(request.body) + if not isinstance(payload, dict): + raise ValueError('Ожидается JSON-объект.') + action = payload.get('action') + if action not in ('preview', 'rerun', 'save'): + raise ValueError('Неизвестное действие.') + raw_settings = payload.get('settings', {}) + if not isinstance(raw_settings, dict): + raise ValueError('Некорректные настройки.') + normalized = service.validate_settings(raw_settings, require_dates=action != 'save') + keys = _selected_keys(payload) + if action == 'preview': + results = service.preview_course_reruns(request.user, keys, normalized) + targets = {row['source_course_id']: row['course_id'] for row in results if row['state'] == 'preview'} + token = signing.dumps({ + 'user_id': request.user.pk, 'course_ids': [str(key) for key in keys], + 'settings': raw_settings, 'targets': targets, + }, salt=PREVIEW_SALT, compress=True) + return JsonResponse({'results': results, 'preview_token': token}) + if action == 'rerun': + token = payload.get('preview_token') + if not isinstance(token, str): + raise ValueError('Сначала выполните предварительный просмотр перезапуска.') + preview = signing.loads(token, salt=PREVIEW_SALT, max_age=3600) + if (preview['user_id'] != request.user.pk or preview['settings'] != raw_settings + or set(preview['course_ids']) != {str(key) for key in keys} + or set(preview['targets']) != {str(key) for key in keys}): + raise ValueError('Выбор курсов или настройки изменились. Повторите предварительный просмотр.') + results = service.queue_course_reruns(request.user, keys, normalized, + expected_course_ids=preview['targets']) + else: + if not normalized: + raise ValueError('Выберите хотя бы одну настройку для сохранения.') + results = service.save_course_settings(request.user, keys, normalized) + LOG.info('Course admin action=%s user=%s courses=%s settings=%s results=%s', action, request.user.pk, + [str(key) for key in keys], normalized, + [(row['course_id'], row['state']) for row in results]) + return JsonResponse({'results': results}) + except (signing.BadSignature, KeyError): + return JsonResponse({ + 'error': 'Предварительный просмотр истёк или недействителен. Выполните его снова.', + }, status=400) + except (ValueError, TypeError, InvalidKeyError, ValidationError) as exc: + message = '; '.join(exc.messages) if isinstance(exc, ValidationError) else str(exc) + return JsonResponse({'error': message}, status=400) + + +@never_cache +@admin_required +@require_GET +def course_admin_status(request): + """Fetch durable rerun results without relying on the Celery result backend.""" + try: + identifiers = request.GET.get('course_ids', '').split(',') + if not 1 <= len(identifiers) <= 50: + raise ValueError('Запрашивайте от 1 до 50 статусов за один запрос.') + keys = [CourseKey.from_string(identifier) for identifier in identifiers] + except (ValueError, InvalidKeyError) as exc: + return JsonResponse({'error': str(exc)}, status=400) + states = {str(state.course_key): state for state in CourseRerunState.objects.find_all(course_key__in=keys)} + return JsonResponse({'results': [ + _state_result(states[str(key)]) if str(key) in states else { + 'source_course_id': '', 'course_id': str(key), 'state': 'unknown', + 'message': 'Запись о перезапуске не найдена. Обновите страницу.', + } for key in keys + ]}) diff --git a/cms/djangoapps/contentstore/views/tests/test_course_admin.py b/cms/djangoapps/contentstore/views/tests/test_course_admin.py new file mode 100644 index 000000000000..a711fc73f609 --- /dev/null +++ b/cms/djangoapps/contentstore/views/tests/test_course_admin.py @@ -0,0 +1,322 @@ +"""HTTP authorization, signed previews and selection rules for the private course console.""" + +import json +from datetime import datetime, timezone +from types import SimpleNamespace +from unittest.mock import patch + +from django.conf import settings +from django.core import signing +from django.core.exceptions import ValidationError +from django.http import HttpResponse +from django.middleware.csrf import get_token +from django.test import RequestFactory, SimpleTestCase, override_settings +from opaque_keys.edx.locator import CourseLocator + +from cms.djangoapps.contentstore.views import course_admin as views + + +SOURCE = 'course-v1:FBB+BNJjTSOB+2026_C1' +TARGET = 'course-v1:FBB+BNJjTSOB+2026_C2' +OTHER = 'course-v1:FBB+OTHER+2026_C1' +DATES = {'start': '2026-09-01T00:00:00Z', 'end': '2026-12-31T00:00:00Z'} + + +def course(run='2026_C1', org='FBB', number='BNJjTSOB', name='Biotechnology', start=None): + """Create an inventory row without a database or a Mongo course.""" + return { + 'key': CourseLocator(org=org, course=number, run=run), 'name': name, + 'start': start, 'end': None, 'enrollment_start': None, 'enrollment_end': None, + 'faculty': 'Biology', 'visibility': 'both', + } + + +@override_settings(COURSE_ADMIN_MAX_BATCH_SIZE=500) +class CourseAdminViewTests(SimpleTestCase): + """Exercise real Django decorators, requests and signatures with service boundaries mocked.""" + + def setUp(self): + super().setUp() + self.factory = RequestFactory() + self.user = SimpleNamespace(pk=10, id=10, is_authenticated=True, is_active=True, is_staff=True) + self.source = CourseLocator.from_string(SOURCE) + self.preview_rows = [{ + 'source_course_id': SOURCE, 'course_id': TARGET, 'state': 'preview', 'message': 'Ready', + }] + self.inventory = self.enterContext(patch.object(views, '_courses', return_value=[course()])) + self.validate = self.enterContext(patch.object( + views.service, 'validate_settings', side_effect=lambda values, **kwargs: dict(values), + )) + self.preview = self.enterContext(patch.object( + views.service, 'preview_course_reruns', return_value=self.preview_rows, + )) + self.queue = self.enterContext(patch.object(views.service, 'queue_course_reruns', return_value=[])) + self.save = self.enterContext(patch.object(views.service, 'save_course_settings', return_value=[])) + self.states = self.enterContext(patch.object(views.CourseRerunState, 'objects')) + self.modes = self.enterContext(patch.object(views.CourseMode, 'objects')) + self.modes.filter.return_value.order_by.return_value = [] + self.generation = self.enterContext(patch.object( + views, 'get_self_generation_enabled_for_courses', return_value={}, + )) + + def request(self, method='post', payload=None, user=None, csrf=True, **extra): + """Build a request with a real, matched CSRF cookie and masked header token.""" + if method == 'post': + request = self.factory.post('/course-admin/action/', json.dumps(payload), + content_type='application/json', **extra) + else: + request = getattr(self.factory, method)('/course-admin/', payload or {}, **extra) + request.user = user or self.user + if csrf: + token = get_token(request) + request.COOKIES[settings.CSRF_COOKIE_NAME] = request.META['CSRF_COOKIE'] + request.META['HTTP_X_CSRFTOKEN'] = token + return request + + def action(self, action='preview', **values): + payload = {'action': action, 'course_ids': [SOURCE], 'settings': dict(DATES)} + payload.update(values) + return views.course_admin_action(self.request(payload=payload)) + + @staticmethod + def body(response): + return json.loads(response.content) + + def token(self, **changes): + data = {'user_id': self.user.pk, 'course_ids': [SOURCE], 'settings': dict(DATES), + 'targets': {SOURCE: TARGET}} + data.update(changes) + return signing.dumps(data, salt=views.PREVIEW_SALT) + + def test_every_endpoint_rejects_anonymous_nonstaff_and_inactive_admins(self): + for endpoint in (views.course_admin_page, views.course_admin_action, views.course_admin_status): + for overrides in ({'is_authenticated': False}, {'is_staff': False}, {'is_active': False}): + with self.subTest(endpoint=endpoint.__name__, user=overrides): + user = SimpleNamespace(**(vars(self.user) | overrides)) + response = endpoint(self.request(payload={}, user=user, csrf=False)) + self.assertEqual(response.status_code, 403) + self.assertIn('no-store', response['Cache-Control']) + self.inventory.assert_not_called() + self.validate.assert_not_called() + self.preview.assert_not_called() + self.queue.assert_not_called() + self.save.assert_not_called() + self.states.find_all.assert_not_called() + + def test_read_endpoints_reject_post_and_write_endpoint_rejects_get(self): + for endpoint in (views.course_admin_page, views.course_admin_status): + with self.subTest(endpoint=endpoint.__name__): + self.assertEqual(endpoint(self.request(payload={})).status_code, 405) + self.assertEqual(views.course_admin_action(self.request(method='get')).status_code, 405) + self.validate.assert_not_called() + + def test_action_requires_csrf_for_each_operation(self): + for operation in ('preview', 'rerun', 'save'): + with self.subTest(operation=operation): + request = self.request(payload={'action': operation}, csrf=False) + self.assertEqual(views.course_admin_action(request).status_code, 403) + self.validate.assert_not_called() + + def test_action_rejects_wrong_csrf_and_untrusted_origin(self): + request = self.request(payload={'action': 'save'}) + request.META['HTTP_X_CSRFTOKEN'] = 'x' * 64 + self.assertEqual(views.course_admin_action(request).status_code, 403) + request = self.request(payload={'action': 'save'}, HTTP_ORIGIN='https://untrusted.example') + self.assertEqual(views.course_admin_action(request).status_code, 403) + self.validate.assert_not_called() + + def test_preview_is_read_only_and_signs_exact_selection_settings_and_targets(self): + response = self.action() + self.assertEqual(response.status_code, 200) + body = self.body(response) + self.assertEqual(body['results'], self.preview_rows) + decoded = signing.loads(body['preview_token'], salt=views.PREVIEW_SALT) + self.assertEqual(decoded, {'user_id': self.user.pk, 'course_ids': [SOURCE], + 'settings': DATES, 'targets': {SOURCE: TARGET}}) + self.preview.assert_called_once_with(self.user, [self.source], DATES) + self.validate.assert_called_once_with(DATES, require_dates=True) + self.queue.assert_not_called() + self.save.assert_not_called() + self.assertIn('no-store', response['Cache-Control']) + + def test_rerun_forwards_preview_mapping_and_repeated_token_unchanged(self): + token = self.body(self.action())['preview_token'] + for _ in range(2): + self.assertEqual(self.action('rerun', preview_token=token).status_code, 200) + self.assertEqual(self.queue.call_count, 2) + for call in self.queue.call_args_list: + self.assertEqual(call.args, (self.user, [self.source], DATES)) + self.assertEqual(call.kwargs, {'expected_course_ids': {SOURCE: TARGET}}) + # The service owns reservation idempotency; the HTTP layer must not recompute a new target. + self.assertEqual(self.preview.call_count, 1) + + def test_rerun_rejects_missing_corrupted_and_expired_tokens(self): + with patch('django.core.signing.time.time', return_value=1000): + expired = self.token() + for token in (None, '', 'corrupt', self.token() + 'x', expired): + with self.subTest(token=token): + self.assertEqual(self.action('rerun', preview_token=token).status_code, 400) + self.queue.assert_not_called() + + def test_rerun_rejects_cross_user_changed_settings_changed_ids_and_partial_preview(self): + variants = ( + {'user_id': 20}, {'settings': dict(DATES, catalog_visibility='none')}, + {'course_ids': [OTHER]}, {'targets': {}}, {'targets': {SOURCE: TARGET, OTHER: OTHER}}, + ) + for changes in variants: + with self.subTest(changes=changes): + response = self.action('rerun', preview_token=self.token(**changes)) + self.assertEqual(response.status_code, 400) + self.queue.assert_not_called() + + def test_preview_with_one_failed_course_cannot_be_submitted_as_a_complete_batch(self): + self.preview.return_value = self.preview_rows + [{ + 'source_course_id': OTHER, 'course_id': None, 'state': 'failed', 'message': 'Missing', + }] + ids = [SOURCE, OTHER] + response = self.action(course_ids=ids) + self.assertEqual(response.status_code, 200) + response = self.action('rerun', course_ids=ids, preview_token=self.body(response)['preview_token']) + self.assertEqual(response.status_code, 400) + self.queue.assert_not_called() + + def test_explicit_selection_deduplicates_ids_and_preserves_order(self): + self.assertEqual(self.action(course_ids=[SOURCE, OTHER, SOURCE]).status_code, 200) + self.assertEqual(self.preview.call_args.args[1], [self.source, CourseLocator.from_string(OTHER)]) + + def test_all_matching_selects_server_inventory_beyond_one_display_page(self): + self.inventory.return_value = [course(number=f'course{index}') for index in range(60)] + response = self.action(all_matching=True, filters={'latest': False}, course_ids=[OTHER]) + self.assertEqual(response.status_code, 200) + self.assertEqual(len(self.preview.call_args.args[1]), 60) + self.assertNotIn(CourseLocator.from_string(OTHER), self.preview.call_args.args[1]) + + def test_changed_all_matching_inventory_invalidates_preview(self): + token = self.body(self.action(all_matching=True, filters={}))['preview_token'] + self.inventory.return_value.append(course(number='NEW')) + response = self.action('rerun', all_matching=True, filters={}, preview_token=token) + self.assertEqual(response.status_code, 400) + self.queue.assert_not_called() + + @override_settings(COURSE_ADMIN_MAX_BATCH_SIZE=1) + def test_empty_and_oversized_selections_rejected_before_service_action(self): + for identifiers in ([], [SOURCE, OTHER]): + with self.subTest(identifiers=identifiers): + self.assertEqual(self.action(course_ids=identifiers).status_code, 400) + self.inventory.return_value = [course(), course(number='OTHER')] + self.assertEqual(self.action(all_matching=True).status_code, 400) + self.preview.assert_not_called() + + def test_malformed_payload_settings_filters_and_identifiers_rejected(self): + variants = ( + {'action': 'delete'}, {'settings': []}, {'course_ids': SOURCE}, {'course_ids': [1]}, + {'course_ids': ['not-a-course']}, {'course_ids': ['FBB/BNJjTSOB/2026_C1']}, + {'course_ids': ['library-v1:FBB+LIB']}, {'all_matching': 'true'}, + {'all_matching': True, 'filters': []}, {'all_matching': True, 'filters': {'year': '26'}}, + ) + for values in variants: + with self.subTest(values=values): + self.assertEqual(self.action(**values).status_code, 400) + request = self.factory.post('/course-admin/action/', '{', content_type='application/json') + request.user = self.user + request._dont_enforce_csrf_checks = True # Test JSON decoding independently of CSRF tests above. + self.assertEqual(views.course_admin_action(request).status_code, 400) + self.assertEqual(views.course_admin_action(self.request(payload=[])).status_code, 400) + self.preview.assert_not_called() + self.queue.assert_not_called() + self.save.assert_not_called() + + def test_service_settings_validation_errors_return_400_without_mutation(self): + self.validate.side_effect = ValidationError(['Invalid start date', 'Invalid visibility']) + response = self.action('save', settings={'start': 'bad', 'catalog_visibility': 'public'}) + self.assertEqual(response.status_code, 400) + self.assertIn('Invalid start date', self.body(response)['error']) + self.save.assert_not_called() + + def test_save_accepts_partial_settings_without_preview_and_rejects_empty_settings(self): + response = self.action('save', settings={'catalog_visibility': 'none'}) + self.assertEqual(response.status_code, 200) + self.validate.assert_called_with({'catalog_visibility': 'none'}, require_dates=False) + self.save.assert_called_once_with(self.user, [self.source], {'catalog_visibility': 'none'}) + self.assertEqual(self.action('save', settings={}).status_code, 400) + self.assertEqual(self.save.call_count, 1) + self.queue.assert_not_called() + + def test_page_paginates_and_sets_csrf_cookie(self): + self.inventory.return_value = [course(number=f'course{index}') for index in range(60)] + self.states.find_all.return_value.order_by.return_value = [] + with patch.object(views, 'render_to_response', return_value=HttpResponse('console')) as render, \ + patch.object(views, '_display_course', side_effect=lambda row, modes, generation: { + 'id': str(row['key']), + }), \ + patch.object(views, 'reverse', return_value='/endpoint/'): + response = views.course_admin_page(self.request(method='get', payload={'page': '2'})) + self.assertEqual(response.status_code, 200) + context = render.call_args.args[1] + self.assertEqual((context['total'], context['num_pages'], context['page']), (60, 2, 2)) + self.assertEqual(len(context['courses']), 10) + self.assertIn(settings.CSRF_COOKIE_NAME, response.cookies) + self.assertTrue(context['csrf_token']) + + def test_page_rejects_invalid_filters_before_inventory_read(self): + response = views.course_admin_page(self.request(method='get', payload={'year': 'bad'})) + self.assertEqual(response.status_code, 400) + self.inventory.assert_not_called() + + def test_status_returns_persisted_state_and_unknown_without_task_backend(self): + state = SimpleNamespace(source_course_key=self.source, course_key=CourseLocator.from_string(TARGET), + state='failed') + self.states.find_all.return_value = [state] + response = views.course_admin_status(self.request( + method='get', payload={'course_ids': ','.join((TARGET, OTHER))}, + )) + self.assertEqual(response.status_code, 200) + rows = self.body(response)['results'] + self.assertEqual([(row['course_id'], row['state']) for row in rows], [(TARGET, 'failed'), (OTHER, 'unknown')]) + self.queue.assert_not_called() + self.states.find_all.assert_called_once_with( + course_key__in=[CourseLocator.from_string(TARGET), CourseLocator.from_string(OTHER)], + ) + + def test_status_rejects_missing_invalid_and_oversized_ids(self): + for value in ('', 'invalid', ','.join([SOURCE] * 51)): + with self.subTest(value=value): + response = views.course_admin_status(self.request(method='get', payload={'course_ids': value})) + self.assertEqual(response.status_code, 400) + self.states.find_all.assert_not_called() + + +class CourseAdminFilterTests(SimpleTestCase): + """Selection preserves course families and supports historical run identifiers.""" + + def test_latest_compares_numeric_run_and_keeps_organizations_separate(self): + rows = [course('2026_C2'), course('2026_C10'), course('2025_C20'), course('2026_C1', org='kaznu')] + result = views.filter_courses(rows, views.parse_filters({})) + self.assertEqual({str(row['key']) for row in result}, { + 'course-v1:FBB+BNJjTSOB+2026_C10', 'course-v1:kaznu+BNJjTSOB+2026_C1', + }) + + def test_legacy_runs_remain_selectable_and_use_start_for_latest(self): + rows = [course('C3', start=datetime(2021, 10, 1, tzinfo=timezone.utc)), + course('2022-2023_C1', start=datetime(2022, 11, 7, tzinfo=timezone.utc)), + course('2021C2', start=datetime(2021, 1, 25, tzinfo=timezone.utc))] + selected = views.filter_courses(rows, views.parse_filters({})) + self.assertEqual(selected[0]['key'].run, '2022-2023_C1') + selected = views.filter_courses(rows, views.parse_filters({'run': 'C3'})) + self.assertEqual(selected[0]['key'].run, 'C3') + self.assertEqual(len(views.filter_courses(rows, views.parse_filters({'latest': False}))), 3) + + def test_filters_apply_before_latest_and_match_literals_case_insensitively(self): + rows = [course('2025_C1'), course('2026_C1'), course('2025_C2', org='OTHER')] + selected = views.filter_courses(rows, views.parse_filters({'year': '2025', 'org': 'fbb', 'q': 'biology'})) + self.assertEqual([str(row['key']) for row in selected], ['course-v1:FBB+BNJjTSOB+2025_C1']) + self.assertEqual(views.filter_courses(rows, views.parse_filters({'q': '.*'})), []) + + def test_filter_validation_bounds_and_normalization(self): + for values in ([], {'q': ['x']}, {'q': 'x' * 256}, {'org': None}, {'year': '0000'}, + {'year': '20266'}, {'latest': 'yes'}): + with self.subTest(values=values), self.assertRaises(ValueError): + views.parse_filters(values) + normalized = views.parse_filters({'q': ' Biology ', 'latest': 'false'}) + self.assertEqual(normalized['q'], 'Biology') + self.assertFalse(normalized['latest']) diff --git a/cms/envs/common.py b/cms/envs/common.py index 65425f566597..5fb375d6a407 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -1608,6 +1608,9 @@ # remember to check that you are not creating new # RemovedInDjango19Warnings in the test logs. INSTALLED_APPS = [ + 'lms.djangoapps.news', + 'lms.djangoapps.univerapi', + # Standard apps 'django.contrib.auth', 'django.contrib.contenttypes', @@ -2701,17 +2704,17 @@ DISABLE_DEPRECATED_SIGNUP_URL = False ##### 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 = '3000/5m' +LOGISTRATION_PER_EMAIL_RATELIMIT_RATE = '300/5m' +LOGISTRATION_API_RATELIMIT = '1000/m' +LOGIN_AND_REGISTER_FORM_RATELIMIT = '3000/5m' RESET_PASSWORD_TOKEN_VALIDATE_API_RATELIMIT = '30/7d' RESET_PASSWORD_API_RATELIMIT = '30/7d' ##### REGISTRATION RATE LIMIT SETTINGS ##### -REGISTRATION_VALIDATION_RATELIMIT = '30/7d' -REGISTRATION_RATELIMIT = '60/7d' -OPTIONAL_FIELD_API_RATELIMIT = '10/h' +REGISTRATION_VALIDATION_RATELIMIT = '3000/7d' +REGISTRATION_RATELIMIT = '6000/7d' +OPTIONAL_FIELD_API_RATELIMIT = '100/h' ##### PASSWORD RESET RATE LIMIT SETTINGS ##### PASSWORD_RESET_IP_RATE = '1/m' diff --git a/cms/static/js/course_admin.js b/cms/static/js/course_admin.js new file mode 100644 index 000000000000..20a22b79eb3f --- /dev/null +++ b/cms/static/js/course_admin.js @@ -0,0 +1,373 @@ +/* global fetch */ +(function() { + 'use strict'; + + var root = document.getElementById('course-admin'); + if (!root) { + return; + } + + var settingsForm = document.getElementById('course-settings'); + var courseCheckboxes = Array.from(root.querySelectorAll('input[name="course_ids"]')); + var selectPage = document.getElementById('select-page'); + var allMatching = document.getElementById('all-matching'); + var previewButton = document.getElementById('preview-rerun'); + var rerunButton = document.getElementById('rerun-courses'); + var saveButton = document.getElementById('save-settings'); + var actionMessage = document.getElementById('action-message'); + var pollMessage = document.getElementById('poll-message'); + var resultBody = document.getElementById('operation-results'); + var results = new Map(); + var appliedFilters = JSON.parse(root.dataset.filters); + var total = Number(root.dataset.total); + var busy = false; + var previewSignature = null; + var previewTargets = null; + var previewToken = null; + var pollTimer = null; + var polling = false; + var pollDelay = 3000; + var stateLabels = { + preview: 'Проверено', + queued: 'В очереди', + in_progress: 'Создаётся', + succeeded: 'Готово', + failed: 'Ошибка', + saved: 'Сохранено' + }; + + function selectedIds() { + return courseCheckboxes.filter(function(input) { return input.checked; }) + .map(function(input) { return input.value; }); + } + + function selectedCount() { + return allMatching.checked ? total : selectedIds().length; + } + + function notify(message, kind) { + actionMessage.textContent = message; + actionMessage.className = 'notice' + (kind ? ' ' + kind : ''); + actionMessage.hidden = !message; + } + + function updateControls() { + var count = selectedCount(); + var selectedOnPage = selectedIds().length; + document.getElementById('selection-count').textContent = 'Выбрано: ' + count; + selectPage.checked = allMatching.checked || (courseCheckboxes.length > 0 && selectedOnPage === courseCheckboxes.length); + selectPage.indeterminate = !allMatching.checked && selectedOnPage > 0 && selectedOnPage < courseCheckboxes.length; + selectPage.disabled = busy || allMatching.checked || !courseCheckboxes.length; + allMatching.disabled = busy || !total; + courseCheckboxes.forEach(function(input) { input.disabled = busy || allMatching.checked; }); + previewButton.disabled = busy || !count; + saveButton.disabled = busy || !count; + rerunButton.disabled = busy || !count || !previewSignature; + Array.from(settingsForm.elements).forEach(function(input) { + if (input.tagName !== 'BUTTON' && input.type !== 'hidden') { + var clearInput = settingsForm.elements.namedItem('clear_' + input.name); + input.disabled = busy || Boolean(clearInput && clearInput.checked); + } + }); + root.setAttribute('aria-busy', busy ? 'true' : 'false'); + } + + function invalidatePreview() { + previewSignature = null; + previewTargets = null; + previewToken = null; + document.getElementById('preview-hint').textContent = 'Сначала выберите курсы и проверьте новые ID. После изменения настроек проверку нужно повторить.'; + updateControls(); + } + + function collectPayload(action) { + var settings = {}; + ['start', 'end', 'enrollment_start', 'enrollment_end', 'certificate_available_date'].forEach(function(name) { + var input = settingsForm.elements.namedItem(name); + var clearInput = settingsForm.elements.namedItem('clear_' + name); + if (clearInput && clearInput.checked) { + settings[name] = null; + } else if (input.value) { + // datetime-local has no zone. This form explicitly collects UTC, independent of the browser zone. + var date = new Date(input.value + 'Z'); + if (Number.isNaN(date.getTime())) { + throw new Error('Проверьте дату: ' + name + '.'); + } + settings[name] = date.toISOString(); + } + }); + ['catalog_visibility', 'mode', 'certificate_mode', 'self_paced', 'invitation_only'].forEach(function(name) { + var value = settingsForm.elements.namedItem(name).value; + if (value !== '') { + settings[name] = name === 'self_paced' || name === 'invitation_only' ? value === 'true' : value; + } + }); + if (action !== 'save') { + if (!settings.start || !settings.end) { + throw new Error('Для нового запуска заполните начало и конец курса в UTC.'); + } + settings.mode = settings.mode || 'copy'; + settings.certificate_mode = settings.certificate_mode || 'copy'; + settings.shift_content_dates = settingsForm.elements.namedItem('shift_content_dates').checked; + } else if (!Object.keys(settings).length) { + throw new Error('Заполните хотя бы одно поле или выберите настройку для изменения.'); + } + if (settings.start && settings.end && settings.start >= settings.end) { + throw new Error('Конец курса должен быть позже начала.'); + } + if (settings.enrollment_start && settings.enrollment_end && settings.enrollment_start >= settings.enrollment_end) { + throw new Error('Конец записи должен быть позже начала записи.'); + } + if (!selectedCount()) { + throw new Error('Выберите хотя бы один курс.'); + } + return { + action: action, + course_ids: allMatching.checked ? [] : selectedIds(), + all_matching: allMatching.checked, + filters: appliedFilters, + settings: settings + }; + } + + function signature(payload) { + return JSON.stringify({ + course_ids: payload.course_ids, + all_matching: payload.all_matching, + filters: payload.filters, + settings: payload.settings + }); + } + + async function readResponse(response) { + var body; + try { + body = await response.json(); + } catch (error) { + throw new Error('Сервер вернул неожиданный ответ. Проверьте авторизацию и обновите страницу.'); + } + if (!response.ok) { + var detail = body.error || body.message; + if (typeof detail !== 'string') { + detail = 'Запрос не выполнен (HTTP ' + response.status + ').'; + } + var requestError = new Error(detail); + requestError.status = response.status; + requestError.results = body.results; + throw requestError; + } + if (!Array.isArray(body.results)) { + throw new Error('В ответе сервера отсутствует список результатов. Обновите страницу для проверки операций.'); + } + return body; + } + + function renderResults() { + resultBody.replaceChildren(); + var counts = {}; + results.forEach(function(result) { + var row = document.createElement('tr'); + [result.source_course_id, result.course_id].forEach(function(id) { + var cell = document.createElement('td'); + cell.className = 'result-id'; + var code = document.createElement('code'); + code.textContent = id || '—'; + cell.appendChild(code); + row.appendChild(cell); + }); + var stateCell = document.createElement('td'); + var state = document.createElement('span'); + var knownState = Object.prototype.hasOwnProperty.call(stateLabels, result.state); + state.className = 'status' + (knownState ? ' status-' + result.state : ''); + state.textContent = knownState ? stateLabels[result.state] : (result.state || 'Неизвестно'); + stateCell.appendChild(state); + row.appendChild(stateCell); + var message = document.createElement('td'); + message.className = 'result-message'; + message.textContent = result.message || ''; + row.appendChild(message); + resultBody.appendChild(row); + counts[result.state] = (counts[result.state] || 0) + 1; + }); + document.getElementById('results-panel').hidden = !results.size; + document.getElementById('results-summary').textContent = Object.keys(stateLabels) + .filter(function(state) { return counts[state]; }) + .map(function(state) { return stateLabels[state] + ': ' + counts[state]; }).join(' · '); + } + + function mergeResults(newResults, isPreview) { + if (isPreview) { + results.forEach(function(result, key) { + if (result.state === 'preview') { + results.delete(key); + } + }); + } + (newResults || []).forEach(function(result) { + // A status lookup may return an unknown destination without its source. + // Reuse the source from the pending row so the update replaces it and + // polling does not continue forever under a second map key. + if (!result.source_course_id && result.course_id) { + results.forEach(function(existing) { + if (!result.source_course_id && existing.course_id === result.course_id) { + result = Object.assign({}, result, {source_course_id: existing.source_course_id}); + } + }); + } + if (!Object.prototype.hasOwnProperty.call(stateLabels, result.state)) { + result = Object.assign({}, result, { + state: 'failed', + message: result.message || 'Состояние операции недоступно. Проверьте курс в Studio.' + }); + } + var key = (result.source_course_id || '') + '→' + (result.course_id || ''); + results.set(key, result); + }); + renderResults(); + } + + function pendingIds() { + var ids = []; + results.forEach(function(result) { + if ((result.state === 'queued' || result.state === 'in_progress') && result.course_id) { + ids.push(result.course_id); + } + }); + return Array.from(new Set(ids)); + } + + function schedulePoll() { + if (!pollTimer && !polling && pendingIds().length) { + pollTimer = window.setTimeout(poll, pollDelay); + } + } + + async function poll() { + pollTimer = null; + var ids = pendingIds(); + if (!ids.length) { + return; + } + polling = true; + try { + // Keep status URLs bounded when the administrator selected all matching courses. + for (var offset = 0; offset < ids.length; offset += 20) { + var url = new URL(root.dataset.statusUrl, window.location.href); + url.searchParams.set('course_ids', ids.slice(offset, offset + 20).join(',')); + var response = await fetch(url.toString(), { + credentials: 'same-origin', + cache: 'no-store', + headers: {Accept: 'application/json'} + }); + var body = await readResponse(response); + mergeResults(body.results, false); + } + pollMessage.hidden = true; + pollDelay = 3000; + } catch (error) { + pollMessage.textContent = 'Не удалось обновить состояние: ' + error.message + ' Проверка будет повторена автоматически.'; + pollMessage.hidden = false; + pollDelay = Math.min(pollDelay * 2, 30000); + } finally { + polling = false; + schedulePoll(); + } + } + + async function submitAction(action) { + if (busy || !settingsForm.reportValidity()) { + return; + } + var payload; + try { + payload = collectPayload(action); + if (action === 'rerun' && previewSignature !== signature(payload)) { + throw new Error('Настройки изменились. Сначала повторите проверку новых запусков.'); + } + if (action === 'rerun') { + payload.preview_targets = previewTargets; + payload.preview_token = previewToken; + } + } catch (error) { + notify(error.message, 'error'); + return; + } + busy = true; + updateControls(); + notify(action === 'preview' ? 'Проверяем курсы и новые номера запусков…' : 'Отправляем операцию. Дождитесь ответа сервера…'); + try { + var response = await fetch(root.dataset.endpoint, { + method: 'POST', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + 'X-CSRFToken': settingsForm.elements.namedItem('csrfmiddlewaretoken').value + }, + body: JSON.stringify(payload) + }); + var body = await readResponse(response); + mergeResults(body.results, action === 'preview'); + var failed = body.results.filter(function(result) { return result.state === 'failed'; }).length; + if (action === 'preview') { + var previewReady = Boolean(body.preview_token) && body.results.length > 0 && body.results.every(function(result) { + return result.state === 'preview' && result.source_course_id && result.course_id; + }); + previewSignature = previewReady ? signature(payload) : null; + previewToken = previewReady ? body.preview_token : null; + previewTargets = previewReady ? {} : null; + if (previewReady) { + body.results.forEach(function(result) { previewTargets[result.source_course_id] = result.course_id; }); + } + document.getElementById('preview-hint').textContent = previewReady + ? 'Проверено запусков: ' + body.results.length + '. Новые ID показаны ниже. Нажмите «Создать новые запуски», чтобы начать.' + : 'Проверьте сообщения по каждому курсу и повторите проверку после исправления ошибок.'; + notify(previewReady ? 'Предварительная проверка завершена. Новые курсы ещё не созданы.' : 'Не все курсы прошли проверку. Подробности ниже.', previewReady ? 'success' : 'error'); + } else { + invalidatePreview(); + notify(failed + ? 'Операция завершилась с ошибками для ' + failed + ' курсов. Проверьте результаты ниже.' + : (action === 'rerun' ? 'Запрос принят. Следите за состоянием создания курсов ниже.' : 'Настройки сохранены. Результаты по каждому курсу показаны ниже.'), failed ? 'error' : 'success'); + } + schedulePoll(); + } catch (error) { + var safeRerunRetry = action === 'rerun' && (!error.status || error.status >= 500); + if (!safeRerunRetry) { + invalidatePreview(); + } + if (Array.isArray(error.results)) { + mergeResults(error.results, action === 'preview'); + schedulePoll(); + } + var message = error.message; + if (safeRerunRetry) { + message += ' Можно повторить создание: сохранены те же ID и параметры, чтобы не создать дубликаты.'; + } else if (action !== 'preview') { + message += ' Если запрос успел выполниться, изменения уже применены. Перед повтором обновите страницу и проверьте результаты.'; + } + notify(message, 'error'); + } finally { + busy = false; + updateControls(); + } + } + + selectPage.addEventListener('change', function() { + courseCheckboxes.forEach(function(input) { input.checked = selectPage.checked; }); + invalidatePreview(); + }); + allMatching.addEventListener('change', invalidatePreview); + courseCheckboxes.forEach(function(input) { input.addEventListener('change', invalidatePreview); }); + settingsForm.addEventListener('input', invalidatePreview); + settingsForm.addEventListener('change', invalidatePreview); + document.getElementById('course-filters').addEventListener('input', invalidatePreview); + document.getElementById('course-filters').addEventListener('change', invalidatePreview); + settingsForm.addEventListener('submit', function(event) { event.preventDefault(); }); + previewButton.addEventListener('click', function() { submitAction('preview'); }); + rerunButton.addEventListener('click', function() { submitAction('rerun'); }); + saveButton.addEventListener('click', function() { submitAction('save'); }); + + mergeResults(JSON.parse(root.dataset.initialResults), false); + updateControls(); + schedulePoll(); +}()); diff --git a/cms/templates/course-admin.html b/cms/templates/course-admin.html new file mode 100644 index 000000000000..2c0234386020 --- /dev/null +++ b/cms/templates/course-admin.html @@ -0,0 +1,228 @@ +## coding=utf-8 +<%page expression_filter="h"/> +<%namespace name="static" file="static_content.html"/> +<%! +import json +from urllib.parse import urlencode +%> +<% + pagination_filters = {key: value for key, value in course_filters.items() if value not in ('', None, False)} + pagination_filters['latest'] = '1' if course_filters.get('latest') else '0' + visibility_labels = {'both': 'Каталог и страница курса', 'about': 'Только страница курса', 'none': 'Скрыт'} +%> + + + + + + + Управление курсами — Studio + + + + +
+
+
Studio · Только для администраторов
+

Управление курсами

+

Выберите курсы, проверьте новые номера запусков и создайте их с общими настройками. + Здесь же можно изменить даты, видимость, режимы обучения и сертификаты существующих курсов.

+
+ + +
+

1. Выберите курсы

Найдено: ${total}
+
+
+ + + + +
+
+ + + +
+
+
+ + + Выбрано: 0 +
+
+ + + + + % for course in courses: + + + + + + + + + % endfor + % if not courses: + + % endif + +
Курсы, соответствующие применённым фильтрам
ВыборКурсОрганизация / запускОбучение (${timezone_label})Запись (${timezone_label})Видимость
${course['name']}${course['id']} + % if course.get('mode_labels'): +
Режимы: ${course['mode_labels']}
+ % endif + % if course.get('certificate_generation'): +
Генерация сертификатов: ${course['certificate_generation']}
+ % endif + % if course.get('self_paced_label'): +
${course['self_paced_label']}
+ % endif + % if course.get('invitation_only_label'): +
${course['invitation_only_label']}
+ % endif + % if course.get('certificate_available_date'): +
Сертификаты с: ${course['certificate_available_date']} UTC
+ % endif +
${course['org']}
${course['run']} + % if course.get('faculty'): +
${course['faculty']}
+ % endif +
С: ${course.get('start') or '—'}До: ${course.get('end') or '—'}С: ${course.get('enrollment_start') or '—'}До: ${course.get('enrollment_end') or '—'}${visibility_labels.get(course.get('visibility'), course.get('visibility') or '—')}
Курсы не найдены. Измените фильтры.
+
+ % if num_pages > 1: + + % endif +

Выбор отдельных курсов действует на текущей странице. «Все найденные курсы» охватывает все страницы применённых фильтров.

+
+ +
+

2. Задайте общие настройки

+

При сохранении существующих курсов пустые поля и «Без изменений» сохраняют текущие настройки. Для нового запуска обязательны начало и конец курса.

+
+ +
+ Расписание · ${timezone_label} +
+ + +
+
+
+

Введите время в UTC. Например, 09:00 по времени Алматы (UTC+5) = 04:00 UTC.

+

В новом запуске пустые даты записи и доступности сертификатов снимают прежние ограничения по датам.

+
+
+ Доступ и обучение +
+ + + + + +
+
+

Включение генерации требует активного настроенного сертификата курса, а также включённых системных настроек HTML-сертификатов и генерации. Выбор режима Honor сам по себе не настраивает сертификат.

+

Видимость управляет каталогом и страницей описания курса. Она не закрывает учебные материалы для уже записанных студентов.

+
+
+ Расписание содержимого при перезапуске + +

Действует только при создании нового запуска. Календари внешних инструментов и задания ORA могут требовать отдельной проверки.

+
+
Новый ID сохраняет организацию и код курса. Год берётся из даты начала в UTC, а номер C выбирается следующим свободным: например, 2026_C1 → 2026_C2. Материалы копируются штатным перезапуском Studio. Если отключить перенос, даты разделов и заданий останутся прежними.
+
+ + +
+

Сначала выберите курсы и проверьте новые ID. После изменения настроек проверку нужно повторить.

+
+

Изменить выбранные существующие курсы
Применяет заполненные поля к выбранным курсам. Новые запуски не создаются.

+ +
+
+ +
+ + +
+ + diff --git a/cms/urls.py b/cms/urls.py index d01e89d9d276..020264fa574d 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -20,6 +20,11 @@ from cms.djangoapps.contentstore import views as contentstore_views from cms.djangoapps.contentstore.views.block import xblock_edit_view from cms.djangoapps.contentstore.views.organization import OrganizationListView +from cms.djangoapps.contentstore.views.course_admin import ( + course_admin_action, + course_admin_page, + course_admin_status, +) from openedx.core.apidocs import api_info from openedx.core.djangoapps.password_policy import compliance as password_policy_compliance from openedx.core.djangoapps.password_policy.forms import PasswordPolicyAwareAdminAuthForm @@ -69,6 +74,10 @@ path('server_error', contentstore_views.server_error, name='server_error'), path('organizations', OrganizationListView.as_view(), name='organizations'), path('api/toggles/', include('openedx.core.djangoapps.waffle_utils.urls')), + # Deliberately absent from Studio navigation; every endpoint requires global staff. + path('course-admin/', course_admin_page, name='course_admin'), + path('course-admin/action/', course_admin_action, name='course_admin_action'), + path('course-admin/status/', course_admin_status, name='course_admin_status'), # noop to squelch ajax errors path('event', contentstore_views.event, name='event'), diff --git a/lms/djangoapps/certificates/api.py b/lms/djangoapps/certificates/api.py index d6c77f4e721b..ee3838d314da 100644 --- a/lms/djangoapps/certificates/api.py +++ b/lms/djangoapps/certificates/api.py @@ -355,6 +355,42 @@ def set_cert_generation_enabled(course_key, is_enabled): log.info("Disabled self-generated certificates for course '%s'.", str(course_key)) +def get_course_certificate_generation_settings(course_key): + """Return the latest per-course certificate-generation settings as plain data.""" + course_settings = CertificateGenerationCourseSetting.get(course_key) + if course_settings is None: + return None + return { + "self_generation_enabled": course_settings.self_generation_enabled, + "language_specific_templates_enabled": course_settings.language_specific_templates_enabled, + "include_hours_of_effort": course_settings.include_hours_of_effort, + } + + +def get_self_generation_enabled_for_courses(course_keys): + """Return the effective latest self-generation flag for each supplied course.""" + result = {} + course_settings = CertificateGenerationCourseSetting.objects.filter( + course_key__in=course_keys, + ).order_by("created", "pk") + for item in course_settings: + result[str(item.course_key)] = item.self_generation_enabled + return result + + +def create_course_certificate_generation_settings(course_key, values): + """Append per-course settings so historical duplicate rows remain well-defined.""" + allowed = { + "self_generation_enabled", + "language_specific_templates_enabled", + "include_hours_of_effort", + } + unknown = set(values) - allowed + if unknown: + raise ValueError(f"Unsupported certificate generation settings: {sorted(unknown)}") + return CertificateGenerationCourseSetting.objects.create(course_key=course_key, **values) + + def is_certificate_invalidated(student, course_key): """Check whether the certificate belonging to the given student (in given course) has been invalidated. diff --git a/lms/djangoapps/certificates/tests/test_generation_settings_api.py b/lms/djangoapps/certificates/tests/test_generation_settings_api.py new file mode 100644 index 000000000000..71001f6978c7 --- /dev/null +++ b/lms/djangoapps/certificates/tests/test_generation_settings_api.py @@ -0,0 +1,41 @@ +"""Tests for the public certificate-generation settings API.""" + +from django.test import TestCase +from opaque_keys.edx.locator import CourseLocator + +from lms.djangoapps.certificates import api + + +class CertificateGenerationSettingsApiTests(TestCase): + """The API handles historical duplicate rows using the most recent value.""" + + def setUp(self): + super().setUp() + self.course_key = CourseLocator(org='FBB', course='BNJjTSOB', run='2026_C1') + + def test_create_and_get_latest_settings(self): + api.create_course_certificate_generation_settings(self.course_key, { + 'self_generation_enabled': False, + 'language_specific_templates_enabled': False, + 'include_hours_of_effort': None, + }) + api.create_course_certificate_generation_settings(self.course_key, { + 'self_generation_enabled': True, + 'language_specific_templates_enabled': True, + 'include_hours_of_effort': True, + }) + + result = api.get_course_certificate_generation_settings(self.course_key) + + self.assertEqual(result, { + 'self_generation_enabled': True, + 'language_specific_templates_enabled': True, + 'include_hours_of_effort': True, + }) + self.assertEqual(api.get_self_generation_enabled_for_courses([self.course_key]), { + str(self.course_key): True, + }) + + def test_create_rejects_unknown_fields(self): + with self.assertRaises(ValueError): + api.create_course_certificate_generation_settings(self.course_key, {'enabled': True}) 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_admin/__init__.py b/lms/djangoapps/course_admin/__init__.py new file mode 100644 index 000000000000..135fa595155f --- /dev/null +++ b/lms/djangoapps/course_admin/__init__.py @@ -0,0 +1 @@ +"""Private LMS course administration page.""" diff --git a/lms/djangoapps/course_admin/views.py b/lms/djangoapps/course_admin/views.py new file mode 100644 index 000000000000..1983e4c2b61e --- /dev/null +++ b/lms/djangoapps/course_admin/views.py @@ -0,0 +1,309 @@ +"""Plain HTML course-run administration for global LMS staff.""" + +import logging +import re +from datetime import timezone +from functools import wraps +from urllib.parse import urlencode + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.paginator import Paginator +from django.http import HttpResponseForbidden, HttpResponseRedirect +from django.middleware.csrf import get_token +from django.urls import reverse +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.http import require_http_methods +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey +from opaque_keys.edx.locator import CourseLocator + +from cms.djangoapps.contentstore import course_admin as service +from common.djangoapps.course_action_state.models import CourseRerunState +from common.djangoapps.course_modes.models import CourseMode +from common.djangoapps.edxmako.shortcuts import render_to_response +from common.djangoapps.student.roles import GlobalStaff +from lms.djangoapps.certificates.api import get_self_generation_enabled_for_courses +from openedx.core.djangoapps.content.course_overviews.models import CourseOverview + + +LOG = logging.getLogger(__name__) +RUN_PATTERN = re.compile(r'^(\d{4})_C([1-9]\d*)$', re.IGNORECASE) +DATE_FIELDS = ('start', 'end', 'enrollment_start', 'enrollment_end', 'certificate_available_date') +FORM_VALUE_FIELDS = DATE_FIELDS + ( + 'catalog_visibility', 'mode', 'certificate_mode', 'self_paced', 'invitation_only', +) +FORM_CHECKBOX_FIELDS = ( + 'clear_enrollment_start', 'clear_enrollment_end', 'clear_certificate_available_date', + 'shift_content_dates', 'hide_source', +) +FORM_SESSION_KEY = 'course_admin.form.v1' +FEEDBACK_SESSION_KEY = 'course_admin.feedback.v1' +FORM_DEFAULTS = {'mode': 'copy', 'certificate_mode': 'copy', 'shift_content_dates': '1'} + + +def admin_required(view): + """Allow only active global platform staff.""" + @wraps(view) + def wrapped(request, *args, **kwargs): + allowed = ( + request.user.is_authenticated + and request.user.is_active + and GlobalStaff().has_user(request.user) + ) + if not allowed: + return HttpResponseForbidden('Доступ разрешён только администраторам платформы.') + return view(request, *args, **kwargs) + return wrapped + + +def parse_filters(values): + """Read the small set of supported list filters from GET or POST data.""" + result = {} + for field in ('q', 'org', 'run', 'year'): + value = values.get(field, '') + if not isinstance(value, str) or len(value) > 255: + raise ValueError('Некорректное значение фильтра.') + result[field] = value.strip() + if result['year'] and not re.fullmatch(r'[1-9]\d{3}', result['year']): + raise ValueError('Год должен состоять из четырёх цифр.') + latest = values.get('latest', '1') + if latest not in ('0', '1', 'true', 'false', 'on', ''): + raise ValueError('Некорректный фильтр последнего запуска.') + result['latest'] = latest in ('1', 'true', 'on') + return result + + +def _courses(): + """Return the LMS SQL course inventory used by the existing reports.""" + courses = [] + for overview in CourseOverview.objects.all(): + key = overview.id + if type(key) is not CourseLocator or key.deprecated: # pylint: disable=unidiomatic-typecheck + continue + courses.append({ + 'key': key, + 'name': overview.display_name or str(key), + 'start': getattr(overview, 'start', None), + 'end': getattr(overview, 'end', None), + 'enrollment_start': getattr(overview, 'enrollment_start', None), + 'enrollment_end': getattr(overview, 'enrollment_end', None), + 'visibility': getattr(overview, 'catalog_visibility', None), + 'faculty': getattr(overview, 'faculty', ''), + 'self_paced': getattr(overview, 'self_paced', False), + 'invitation_only': getattr(overview, 'invitation_only', False), + 'certificate_available_date': getattr(overview, 'certificate_available_date', None), + }) + return courses + + +def _run_order(course): + match = RUN_PATTERN.fullmatch(course['key'].run) + start = course.get('start') + year = int(match[1]) if match else (start.year if start else 0) + sequence = int(match[2]) if match else 0 + return year, sequence, str(start or ''), course['key'].run + + +def filter_courses(courses, filters): + """Apply literal filters and optionally keep the latest run per course family.""" + selected = [] + for course in courses: + key = course['key'] + if filters['org'] and filters['org'].casefold() != key.org.casefold(): + continue + if filters['run'] and filters['run'].casefold() != key.run.casefold(): + continue + if filters['year'] and not key.run.startswith(filters['year']): + continue + searchable = f"{key} {course['name']} {course.get('faculty') or ''}".casefold() + if filters['q'] and filters['q'].casefold() not in searchable: + continue + selected.append(course) + if filters['latest']: + families = {} + for course in selected: + key = course['key'] + family = (key.org.casefold(), key.course.casefold()) + previous = families.get(family) + if previous is None or _run_order(course) > _run_order(previous): + families[family] = course + selected = list(families.values()) + return sorted(selected, key=lambda row: (row['name'].casefold(), str(row['key']))) + + +def _format_date(value): + if not value: + return '' + return value.astimezone(timezone.utc).strftime('%Y-%m-%d %H:%M') + + +def _display_courses(courses): + keys = [course['key'] for course in courses] + modes = {} + for mode in CourseMode.objects.filter(course_id__in=keys).order_by('mode_slug', 'currency'): + modes.setdefault(str(mode.course_id), []).append(mode.mode_slug) + generation = get_self_generation_enabled_for_courses(keys) + rows = [] + for course in courses: + key = course['key'] + rows.append({ + **course, + 'id': str(key), + 'org': key.org, + 'run': key.run, + 'about_url': reverse('about_course', kwargs={'course_id': str(key)}), + 'mode_labels': ', '.join(modes.get(str(key), [])) or CourseMode.DEFAULT_MODE_SLUG, + 'certificate_generation': 'Включена' if generation.get(str(key)) else 'Выключена', + 'start': _format_date(course['start']), + 'end': _format_date(course['end']), + 'enrollment_start': _format_date(course['enrollment_start']), + 'enrollment_end': _format_date(course['enrollment_end']), + 'certificate_available_date': _format_date(course['certificate_available_date']), + }) + return rows + + +def _selected_keys(request, filters, all_courses): + if request.POST.get('all_matching') == '1': + keys = [course['key'] for course in filter_courses(all_courses, filters)] + else: + identifiers = list(dict.fromkeys(request.POST.getlist('course_ids'))) + keys = [] + for identifier in identifiers: + key = CourseKey.from_string(identifier) + if ( + type(key) is not CourseLocator # pylint: disable=unidiomatic-typecheck + or key.deprecated or key.branch or key.version_guid + ): + raise ValueError('Поддерживаются только обычные курсы с идентификатором course-v1.') + keys.append(key) + limit = getattr(settings, 'COURSE_ADMIN_MAX_BATCH_SIZE', 1000) + if not keys or len(keys) > limit: + raise ValueError(f'Выберите от 1 до {limit} курсов.') + return keys + + +def _settings_from_form(request, rerun): + values = {} + for field in DATE_FIELDS: + raw_value = request.POST.get(field, '').strip() + if request.POST.get(f'clear_{field}') == '1': + values[field] = None + elif raw_value: + values[field] = raw_value + for field in ('catalog_visibility', 'mode', 'certificate_mode'): + value = request.POST.get(field, '').strip() + if value: + values[field] = value + for field in ('self_paced', 'invitation_only'): + value = request.POST.get(field, '') + if value in ('true', 'false'): + values[field] = value == 'true' + elif value: + raise ValueError(f'{field}: некорректное значение.') + if rerun: + values['shift_content_dates'] = request.POST.get('shift_content_dates') == '1' + values['hide_source'] = request.POST.get('hide_source') == '1' + return service.validate_settings(values, require_dates=rerun) + + +def _form_values(request): + """Keep an administrator's settings across redirects and later page loads.""" + values = dict(FORM_DEFAULTS) + saved = request.session.get(FORM_SESSION_KEY, {}) + if isinstance(saved, dict): + values.update({key: value for key, value in saved.items() if isinstance(value, str)}) + if request.method == 'POST': + for field in FORM_VALUE_FIELDS: + value = request.POST.get(field, '') + values[field] = value[:255] if isinstance(value, str) else '' + for field in FORM_CHECKBOX_FIELDS: + values[field] = '1' if request.POST.get(field) == '1' else '' + request.session[FORM_SESSION_KEY] = values + return values + + +def _redirect_after_post(filters, page): + query = {key: value for key, value in filters.items() if value not in ('', None, False)} + query['latest'] = '1' if filters.get('latest') else '0' + if page: + query['page'] = page + return HttpResponseRedirect(f"{reverse('course_admin')}?{urlencode(query)}") + + +def _state_result(state): + messages = { + 'succeeded': 'Перезапуск завершён.', + 'failed': 'Ошибка перезапуска. Проверьте журнал фонового обработчика.', + 'in_progress': 'Курс создаётся в фоновой задаче.', + } + return { + 'source_course_id': str(state.source_course_key), + 'course_id': str(state.course_key), + 'state': state.state, + 'message': messages.get(state.state, state.message or state.state), + } + + +@never_cache +@admin_required +@csrf_protect +@require_http_methods(['GET', 'POST']) +def course_admin(request): + """Render and process the complete admin form on one LMS URL.""" + source = request.POST if request.method == 'POST' else request.GET + form_values = _form_values(request) + feedback = request.session.pop(FEEDBACK_SESSION_KEY, {}) if request.method == 'GET' else {} + results = feedback.get('results', []) if isinstance(feedback, dict) else [] + error = feedback.get('error', '') if isinstance(feedback, dict) else '' + try: + filters = parse_filters(source) + except ValueError as exc: + filters = parse_filters({}) + error = error or str(exc) + + all_courses = _courses() + if request.method == 'POST': + if not error: + try: + action = request.POST.get('action') + if action not in ('rerun', 'save'): + raise ValueError('Неизвестное действие.') + keys = _selected_keys(request, filters, all_courses) + normalized = _settings_from_form(request, rerun=action == 'rerun') + if action == 'rerun': + results = service.queue_course_reruns(request.user, keys, normalized) + else: + if not normalized: + raise ValueError('Заполните хотя бы одну настройку для сохранения.') + results = service.save_course_settings(request.user, keys, normalized) + LOG.info( + 'LMS course admin action=%s user=%s courses=%s results=%s', + action, request.user.pk, [str(key) for key in keys], + [(row['course_id'], row['state']) for row in results], + ) + except (ValueError, TypeError, InvalidKeyError, ValidationError) as exc: + error = '; '.join(exc.messages) if isinstance(exc, ValidationError) else str(exc) + except Exception: # pylint: disable=broad-except + LOG.exception('Unexpected LMS course admin failure') + error = 'Операция не выполнена. Подробности записаны в журнал LMS.' + request.session[FEEDBACK_SESSION_KEY] = {'results': results, 'error': error} + return _redirect_after_post(filters, request.POST.get('page')) + + courses = filter_courses(all_courses, filters) + page = Paginator(courses, 50).get_page(request.GET.get('page', 1)) + recent = CourseRerunState.objects.find_all(created_user=request.user).order_by('-created_time')[:30] + return render_to_response('course-admin.html', { + 'courses': _display_courses(page.object_list), + 'course_filters': filters, + 'page': page.number, + 'num_pages': page.paginator.num_pages, + 'total': page.paginator.count, + 'results': results, + 'recent_results': [_state_result(state) for state in recent], + 'form_values': form_values, + 'error': error, + 'csrf_token': get_token(request), + }, request=request) 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..fe0786f45935 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}, + ) + # Do not change courses2 to courses return render_to_response( - "courseware/courses.html", + "courseware/courses2.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/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..4f9ff586aafa --- /dev/null +++ b/lms/djangoapps/news/views.py @@ -0,0 +1,539 @@ +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 CourseEnrollment, UserProfile +from lms.djangoapps.certificates.data import CertificateStatuses +from lms.djangoapps.certificates.models import GeneratedCertificate +from lms.djangoapps.grades.models import PersistentCourseGrade +from opaque_keys import InvalidKeyError +from opaque_keys.edx.keys import CourseKey + +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 Http404, HttpResponseBadRequest, JsonResponse +from django.views.decorators.csrf import csrf_exempt +from django.conf import settings + + +from django.db.models.functions import ExtractYear, TruncMonth +from django.db.models import Avg, Count, Exists, Min, OuterRef, Q + +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, gettext as _ + +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) +# +@user_passes_test(lambda u: u.is_staff) +def analyze(request): + course_org_filter = ["Test_kaznu", "rty", "123", "Demo"] + course_name_filter = ["AI Tools in Action: Boosting Productivity with Modern Workflows"] + + now = timezone.now() + today = now.date() + current_year = today.year + + year_start = now.replace(month=1, day=1, hour=0, minute=0, second=0, microsecond=0) + next_year_start = year_start.replace(year=current_year + 1) + all_courses_qs = ( + CourseOverview.objects + .exclude(org__in=course_org_filter) + .exclude(display_name__in=course_name_filter) + ) + # Count each nonempty title once, even across different course codes and runs. + unique_course_names = { + name.strip() + for name in all_courses_qs.values_list("display_name", flat=True) + if name and name.strip() + } + + # Include every run starting this year, regardless of its end date or title. + current_year_courses = list(all_courses_qs.filter( + start__gte=year_start, + start__lt=next_year_start, + )) + max_year = current_year + 1 + + courses_by_year_qs = ( + all_courses_qs + .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 current_year_courses + if course.faculty + ) + + directions_counter = Counter( + course.directions for course in current_year_courses + if course.directions + ) + + language_counter = Counter( + course.language for course in current_year_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 + ) + + # Group runs across years by course identity, not by their editable titles. + course_run_counts = Counter( + (course_key.org, course_key.course) + for course_key in all_courses_qs.values_list("id", flat=True) + ) + + 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.id.org, course.id.course), 1), + "url": reverse("course_details", kwargs={"course_id": str(course.id)}), + } + for course in top_courses + ] + context = { + "courses_count": len(unique_course_names), + "current_year": current_year, + "current_year_courses_count": len(current_year_courses), + "faculty_count": len(faculty_counter), + "directions_count": len(directions_counter), + "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) + + +@user_passes_test(lambda u: u.is_staff) +def course_details(request, course_id): + """Aggregate selected runs of the same course, deduplicating learner totals.""" + try: + course_key = CourseKey.from_string(course_id) + except InvalidKeyError as exc: + raise Http404("Invalid course ID") from exc + + course = get_object_or_404(CourseOverview, id=course_key) + # The run component and editable display name are not part of course identity. + available_runs = [ + run for run in CourseOverview.objects.filter(org=course_key.org).order_by("-start", "id") + if (run.id.org, run.id.course) == (course_key.org, course_key.course) + ] + available_ids = {str(run.id) for run in available_runs} + if request.GET.get("scope") == "all": + selected_ids = available_ids + elif "selection" in request.GET or "run" in request.GET: + selected_ids = set(request.GET.getlist("run")) + if not selected_ids or not selected_ids.issubset(available_ids): + return HttpResponseBadRequest(_("Select at least one valid run of this course.")) + else: + selected_ids = {str(course_key)} + selected_runs = [run for run in available_runs if str(run.id) in selected_ids] + selected_keys = [run.id for run in selected_runs] + # A single selection displays that run's metadata and course link. + if len(selected_runs) == 1: + course = selected_runs[0] + now = timezone.now() + enrollments = CourseEnrollment.objects.filter(course_id__in=selected_keys).order_by() + enrollment_stats = enrollments.aggregate( + total=Count("user_id", distinct=True), + enrollment_records=Count("id"), + first_enrollment=Min("created"), + active=Count("user_id", filter=Q(is_active=True), distinct=True), + recent=Count("user_id", filter=Q(created__gte=now - timedelta(days=30), created__lte=now), distinct=True), + ) + # Use the same enrollment population for outcomes and their denominators. + # Match BOTH the learner and run, not just enrollment in any selected run. + matching_enrollment = CourseEnrollment.objects.filter( + course_id=OuterRef("course_id"), user_id=OuterRef("user_id"), + ) + certificate_stats = GeneratedCertificate.objects.filter( + Exists(matching_enrollment), + course_id__in=selected_keys, + status=CertificateStatuses.downloadable, + ).aggregate(recipients=Count("user_id", distinct=True), issued=Count("id")) + certificate_count = certificate_stats["recipients"] + grade_stats = PersistentCourseGrade.objects.filter( + Exists(matching_enrollment), + course_id__in=selected_keys, + ).aggregate( + recorded=Count("user_id", distinct=True), + passed=Count("user_id", filter=Q(passed_timestamp__isnull=False), distinct=True), + average=Avg("percent_grade"), + ) + first_enrollment = enrollment_stats.pop("first_enrollment") + total = enrollment_stats["total"] + stats = { + **enrollment_stats, + "inactive": total - enrollment_stats["active"], + "certificates": certificate_count, + "issued_certificates": certificate_stats["issued"], + "certificate_rate": round(certificate_count * 100 / total, 1) if total else 0, + "grades_recorded": grade_stats["recorded"], + "passed": grade_stats["passed"], + "average_grade": round(grade_stats["average"] * 100, 1) if grade_stats["average"] is not None else None, + } + + # Include the complete enrollment history, with empty calendar months filled in. + current_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) + month = min(first_enrollment or now, now).replace(day=1, hour=0, minute=0, second=0, microsecond=0) + months = [] + while month <= current_month: + months.append(month) + month = (month + timedelta(days=32)).replace(day=1) + monthly_counts = { + (row["month"].year, row["month"].month): row["total"] + for row in enrollments.filter(created__gte=months[0], created__lte=now) + .annotate(month=TruncMonth("created", tzinfo=now.tzinfo)) + .values("month") + .annotate(total=Count("id")) + .order_by("month") + } + peak = max(monthly_counts.values(), default=0) + enrollment_history = [] + for month in months: + count = monthly_counts.get((month.year, month.month), 0) + enrollment_history.append({ + "label": month.strftime("%m.%Y"), + "total": count, + "height": round(count * 100 / peak, 1) if peak else 0, + }) + + context = { + "course": course, + "available_runs": available_runs, + "selected_ids": selected_ids, + "selected_runs": selected_runs, + "selection_url": reverse("course_details", kwargs={"course_id": str(course_key)}), + "stats": stats, + "faculty": translate_faculty(course.faculty), + "directions": translate_direction(course.directions), + "enrollment_history": enrollment_history, + "history_total": sum(row["total"] for row in enrollment_history), + "analysis_url": reverse("analyze"), + "course_url": reverse("about_course", kwargs={"course_id": str(course.id)}), + "generated_at": timezone.localtime(now).strftime("%d.%m.%Y %H:%M"), + } + return render_to_response("news/course_details.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/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/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/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"/> -
-

${accomplishment_copy_about}

- -
-

${certificate_info_description}

-
-
diff --git a/lms/templates/certificates/_accomplishment-banner.html b/lms/templates/certificates/_accomplishment-banner.html index 15cab1247697..742907217ca9 100644 --- a/lms/templates/certificates/_accomplishment-banner.html +++ b/lms/templates/certificates/_accomplishment-banner.html @@ -40,53 +40,3 @@
-
- -
diff --git a/lms/templates/certificates/_accomplishment-rendering.html b/lms/templates/certificates/_accomplishment-rendering.html index 999c7a39f460..93876d40dd3f 100644 --- a/lms/templates/certificates/_accomplishment-rendering.html +++ b/lms/templates/certificates/_accomplishment-rendering.html @@ -2,117 +2,337 @@ <%! from django.utils.translation import gettext as _ %> <%namespace name='static' file='../static_content.html'/> <% +from datetime import datetime course_mode_class = course_mode if course_mode else '' + +# Преобразуем дату в формат дд.мм.гггг +try: + date_obj = datetime.strptime(certificate_date_issued, "%B %d, %Y") + formatted_date = date_obj.strftime("%d.%m.%Y") +except: + formatted_date = certificate_date_issued + +# Создаем URL для QR кода +qr_code_url = "https://open.kaznu.kz/certificates/" + certificate_id_number %> +<%! import re %> -
+ + -
-
- - - - + + -
- - Image for course mode of type ${course_mode_class} - + + + +
+
⚠️
+

Внимание пользователям iPhone!

+

На устройствах Apple сертификат может скачиваться некорректно.

+

Рекомендуем: Для гарантированно качественного результата скачайте сертификат с компьютера или ноутбука.

+

+ Если скачиваете с iPhone - проверьте качество файла после загрузки +

+
+ +
+
+
+
+ + +
+ QR Code
-
- +
+ + + + diff --git a/lms/templates/course-admin.html b/lms/templates/course-admin.html new file mode 100644 index 000000000000..7fc59df29c5a --- /dev/null +++ b/lms/templates/course-admin.html @@ -0,0 +1,222 @@ +## coding=utf-8 +<%page expression_filter="h"/> +<%! +from urllib.parse import urlencode + +def selected(values, name, value): + return 'selected' if values.get(name, '') == value else '' + +def checked(values, name): + return 'checked' if values.get(name) == '1' else '' +%> +<% + pagination_filters = {key: value for key, value in course_filters.items() if value not in ('', None, False)} + pagination_filters['latest'] = '1' if course_filters.get('latest') else '0' + visibility_labels = {'both': 'Каталог и страница курса', 'about': 'Только страница курса', 'none': 'Скрыт'} +%> + + + + + + + Управление курсами + + + +
+

Управление курсами

+

Закрытая LMS-страница для массового перезапуска и настройки курсов.

+ + % if error: +
${error}
+ % endif + % if results: +
Команда обработана. Результаты показаны ниже.
+ % endif + +
+

Фильтр курсов

+
+
+ + + + +
+
+ + + + Найдено: ${total} +
+
+
+ +
+ + % for name in ('q', 'org', 'run', 'year'): + + % endfor + + + +
+

1. Выберите курсы

+
+ + Выбрано на странице: 0 +
+
+ + + + % for course in courses: + + + + + + + + + % endfor + % if not courses: + + % endif + +
КурсОрганизация / запускПериод курса, UTCРежим / сертификатыВидимость
${course['name']}${course['id']}${course['org']}
${course['run']}
${course['start'] or '—'}
${course['end'] or '—'}
${course['mode_labels']}
Генерация: ${course['certificate_generation']}
${visibility_labels.get(course.get('visibility'), course.get('visibility') or '—')}
Курсы не найдены.
+
+ % if num_pages > 1: + + % endif +
+ +
+

2. Общие настройки

+

После отправки формы значения сохраняются в вашей администраторской сессии и останутся после обновления страницы.

+
+ Расписание, UTC +
+ + + + +
+
+
+ Доступ и сертификаты +
+ + + + + + +
+
+
+ + +
+
+ + +
+

Для перезапуска укажите начало и конец курса. Новый ID получит следующий свободный номер YYYY_Cn.

+
+
+ + % if results: +
+

Результат команды

+
+ % for result in results: + + % endfor +
Исходный курсЦелевой курсСостояниеСообщение
${result.get('source_course_id') or '—'}${result.get('course_id') or '—'}${result.get('state')}${result.get('message')}
+
+ % endif + + % if recent_results: +
+

Последние перезапуски

+

Открывайте новый курс после состояния «succeeded»: к этому моменту его LMS-оглавление уже построено.

+ Обновить состояния +
+ % for result in recent_results: + + % endfor +
Исходный курсНовый курсСостояниеСообщение
${result['source_course_id']} + % if result['state'] == 'succeeded': + ${result['course_id']} + % else: + ${result['course_id']} + % endif + ${result['state']}${result['message']}
+
+ % endif +
+ + + diff --git a/lms/templates/courseware/course_about.html b/lms/templates/courseware/course_about.html index eec9caeadbec..ac82a6dbfb1d 100644 --- a/lms/templates/courseware/course_about.html +++ b/lms/templates/courseware/course_about.html @@ -65,7 +65,26 @@

${course.display_org_with_default}

${course.display_name_with_default}


-

${get_course_about_section(request, course, 'short_description')}

+ % if same_name_courses: +
+ + +
+% endif + +
@@ -206,6 +225,17 @@

${course.display_name_with_default}

  • ${_("Estimated Effort")}

    ${get_course_about_section(request, course, "effort")}
  • % endif + % if complexity: +
  • + +

    + ${_("Course Difficulty")} +

    + + ${_(complexity)} + +
  • + % endif ##
  • ${_('Course Length')}

    ${_('{number} weeks').format(number=15)}
  • %if course_price and (is_cosmetic_price_enabled): diff --git a/lms/templates/courseware/course_about_sidebar_header.html b/lms/templates/courseware/course_about_sidebar_header.html index 7f59ed1f2ffb..d50075dfa89c 100644 --- a/lms/templates/courseware/course_about_sidebar_header.html +++ b/lms/templates/courseware/course_about_sidebar_header.html @@ -48,14 +48,24 @@ body=six.moves.urllib.parse.quote(email_body.encode('UTF-8')) ) %> +
    % endif diff --git a/lms/templates/courseware/courses.html b/lms/templates/courseware/courses.html index 11ad5079ea65..53aee15827eb 100644 --- a/lms/templates/courseware/courses.html +++ b/lms/templates/courseware/courses.html @@ -1,81 +1,679 @@ <%page expression_filter="h"/> <%! - import json from django.utils.translation import gettext as _ - from openedx.core.djangolib.js_utils import js_escaped_string, dump_js_escaped_json %> <%inherit file="../main.html" /> -<% - course_discovery_enabled = settings.FEATURES.get('ENABLE_COURSE_DISCOVERY') -%> -<%namespace name='static' file='../static_content.html'/> +<%block name="pagetitle">${_("Courses")} + +<%block name="headextra"> + -% endif -<%block name="pagetitle">${_("Courses")} +<%block name="content"> +
    +
    + + -
    -
    -
    - % if course_discovery_enabled: - +
    +
    ${_("Course catalog")}
    +

    ${_("Courses")}

    +

    ${_("Explore our collection of educational programs and start learning today")}

    +
    +
    - -
    -
    -
    + + diff --git a/lms/templates/courseware/courses2.html b/lms/templates/courseware/courses2.html new file mode 100644 index 000000000000..c47fc485b825 --- /dev/null +++ b/lms/templates/courseware/courses2.html @@ -0,0 +1,1040 @@ +<%page expression_filter="h"/> +<%! + from django.utils.translation import gettext as _ +%> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Courses")} + +<%block name="headextra"> + + + +<%block name="content"> +
    + + +
    + + + + + +
    +
    ${_("Course catalog")}
    +

    ${_("Courses")}

    +

    ${_("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') + %> +
    +
    + ${course.display_name or _('Course')} +
    ${status_text}
    +
    + +
    +

    ${course.display_name}

    + +
    + ${course.display_org_with_default} + % if course.language: + ${course.language} + % endif +
    + +
    + ${_('Start')}: ${course.start.strftime('%Y-%m-%d') if course.start else _('Not specified')} + ${_('End')}: ${course.end.strftime('%Y-%m-%d') if course.end else _('Not specified')} +
    + + ${_('Go to course')} +
    +
    + % endfor + % else: +
    + +

    ${_('No courses found')}

    +

    ${_('There are currently no available courses')}

    +
    + % endif +
    +
    +
    + + + + + diff --git a/lms/templates/courseware/courses_basic.html b/lms/templates/courseware/courses_basic.html new file mode 100644 index 000000000000..f8826922a3be --- /dev/null +++ b/lms/templates/courseware/courses_basic.html @@ -0,0 +1,353 @@ +<%page expression_filter="h"/> +<%! + from django.utils.translation import gettext as _ +%> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Courses")} + +<%block name="headextra"> + + + +<%block name="content"> +
    +

    ${_("Courses")}

    +

    ${_("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') + %> +
    +
    + ${course.display_name} +
    ${status_text}
    +
    +
    +

    ${course.display_name}

    +
    + ${course.display_org_with_default} + % if course.language: + ${course.language} + % endif +
    +
    + ${_('Start')}: ${course.start.strftime('%Y-%m-%d') if course.start else _('Not specified')} + ${_('End')}: ${course.end.strftime('%Y-%m-%d') if course.end else _('Not specified')} +
    + ${_('Go to course')} +
    +
    + % endfor + % else: +
    + 📚 +

    ${_('No courses found')}

    +

    ${_('There are currently no available courses')}

    +
    + % endif +
    +
    + + + diff --git a/lms/templates/courseware/courses_summer.html b/lms/templates/courseware/courses_summer.html new file mode 100644 index 000000000000..7438d15fd067 --- /dev/null +++ b/lms/templates/courseware/courses_summer.html @@ -0,0 +1,738 @@ +<%page expression_filter="h"/> +<%! + from django.utils.translation import gettext as _ +%> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Courses")} + +<%block name="headextra"> + + + +<%block name="content"> +
    +
    + + + + + + +
    +
    ${_("Summer of learning")}
    +

    ${_("Courses")}

    +

    ${_("Explore our collection of educational programs and start learning today")}

    +
    +
    + +
    +
    +

    ${_("Find your next course")}

    +
    + + + + + + + +
    +
    + +
    + % 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') + %> +
    +
    + ${course.display_name} +
    ${status_text}
    +
    + +
    +

    ${course.display_name}

    +
    + ${course.display_org_with_default} + % if course.language: + ${course.language} + % endif +
    +
    + ${_('Start')}: ${course.start.strftime('%Y-%m-%d') if course.start else _('Not specified')} + ${_('End')}: ${course.end.strftime('%Y-%m-%d') if course.end else _('Not specified')} +
    + ${_('Go to course')} +
    +
    + % endfor + % else: +
    + +

    ${_('No courses found')}

    +

    ${_('There are currently no available courses')}

    +
    + % endif +
    +
    +
    + + + + + diff --git a/lms/templates/courseware/courses_winter.html b/lms/templates/courseware/courses_winter.html new file mode 100644 index 000000000000..72b206c0f6bb --- /dev/null +++ b/lms/templates/courseware/courses_winter.html @@ -0,0 +1,585 @@ +<%page expression_filter="h"/> +<%! +from django.utils.translation import gettext as _ +%> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Courses")} + +<%block name="headextra"> + + + +<%block name="content"> +
    +

    ${_("Courses")}

    +

    ${_("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') + %> +
    +
    + ${course.display_name} +
    ${status_text}
    +
    +
    +

    ${course.display_name}

    +
    + ${course.display_org_with_default} + % if course.language: + ${course.language} + % endif +
    +
    + ${_('Start')} + : ${course.start.strftime('%Y-%m-%d') if course.start else _('Not specified')} + ${_('End')}: ${course.end.strftime('%Y-%m-%d') if course.end else _('Not specified')} +
    + ${_('Go to course')} +
    +
    + % endfor + % else: +
    + 📚 +

    ${_('No courses found')}

    +

    ${_('There are currently no available courses')}

    +
    + % endif +
    +
    + + + + diff --git a/lms/templates/footer.html b/lms/templates/footer.html index f996030a7179..51a2ad1a8c7a 100644 --- a/lms/templates/footer.html +++ b/lms/templates/footer.html @@ -9,157 +9,109 @@ <% footer = get_footer(is_secure=is_secure) %> <% icp_license_info = getattr(settings, 'ICP_LICENSE_INFO', {})%> <%namespace name='static' file='static_content.html'/> + + - -% endif -% if include_dependencies: - <%static:js group='base_vendor'/> - <%static:css group='style-vendor'/> - <%include file="widgets/segment-io.html" /> - <%include file="widgets/segment-io-footer.html" /> -% endif -% if footer_css_urls: - % for url in footer_css_urls: - - % endfor -% endif diff --git a/lms/templates/header/header.html b/lms/templates/header/header.html index 3fafde87e07f..e5ddd9cb6966 100644 --- a/lms/templates/header/header.html +++ b/lms/templates/header/header.html @@ -1,5 +1,3 @@ -## mako - <%page expression_filter="h" args="online_help_token, use_cookie_banner=False"/> <%namespace name='static' file='../static_content.html'/> @@ -20,6 +18,9 @@ %> ## Provide a hook for themes to inject branding on top. + + + <%block name="navigation_top" /> <% @@ -41,6 +42,26 @@ catch(e){window.attachEvent("onload", $buo_f)} % endif + + + + % if course: +
    + +
    +
    +
    +
    ${_("MOOC")}
    +
    ${_("Online")}
    +
    ${_("Courses")}
    +
    ${_("Open")}
    +
    ${_("Massive")}
    +
    +
    +
    +

    ${_("About MOOC")}

    +

    + ${_("Massive Open Online Courses (MOOCs) are free online courses available to everyone. MOOCs provide a flexible and accessible way to gain new skills, advance your career, and deliver quality education at scale.")} +

    +

    + ${_("Millions of people worldwide use MOOCs for various reasons, including career advancement, career change, college preparation, additional education, lifelong learning, corporate e-learning, training, and much more.")} +

    +

    + ${_("MOOCs have fundamentally changed the way people learn around the world. Ready to get started?")} +

    + + +
    + ${_("Our News")}!
    + ${_("Check out our latest updates on the platform")} +
    +
    + +
    +
    + +
    + + + + + + + + + +
    +
    +

    ${_("Advantages of Online Courses")}

    + +
    +

    + + ${_("Flexible Schedule")} +

    +

    ${_("Learn at your own pace and at a convenient time, balancing studies with work or other commitments.")}

    +
    + +
    +

    + + ${_("Accessibility")} +

    +

    ${_("Get access to high-quality courses from anywhere in the world, without leaving your home.")}

    +
    + +
    +

    + + ${_("Variety of Courses")} +

    +

    ${_("Choose from a wide range of programs: from programming to personal development.")}

    +
    + +
    +

    + + ${_("Certificates")} +

    +

    ${_("Earn official certificates upon course completion to strengthen your resume.")}

    +
    + +
    +

    + + ${_("Community")} +

    +

    ${_("Connect with instructors and students, share experiences, and exchange ideas.")}

    +
    + +
    +

    + + ${_("Free Learning")} +

    +

    ${_("Many courses are available for free, making education accessible to everyone.")}

    +
    +
    +
    diff --git a/lms/templates/main.html b/lms/templates/main.html index 1a11900dae58..85861d47834b 100644 --- a/lms/templates/main.html +++ b/lms/templates/main.html @@ -33,6 +33,7 @@ + ## Define a couple of helper functions to make life easier when ## embedding theme conditionals into templates. All inheriting @@ -132,7 +133,15 @@ ${render_require_js_path_overrides(settings.REQUIRE_JS_PATH_OVERRIDES) | n, decode.utf8} - <%block name="headextra"/> + <%block name="headextra"> + + + <%block name="head_extra"/> <%include file="/courseware/experiments.html"/> @@ -211,7 +220,7 @@
    <%block name="marketing_hero">
    -
    +
    ${next.body()} <%block name="bodyextra"/>
    @@ -248,3 +257,7 @@ next=quote_plus(login_redirect_url if login_redirect_url else request.path) ) if (login_redirect_url or (request and not request.path.startswith("/logout"))) else "" } + + diff --git a/lms/templates/news/analyze.html b/lms/templates/news/analyze.html new file mode 100644 index 000000000000..41fe62b7106e --- /dev/null +++ b/lms/templates/news/analyze.html @@ -0,0 +1,842 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Analyze")} + +<%block name="extrahead"> + + + + +<%block name="content"> +
    +
    +
    +
    +
    ${_("Course analytics")}
    +

    ${_("Course analysis")}

    +

    + ${_("Summary of published courses without test organizations. The current courses of the current year are shown below; schedules of faculties and directions filter the list.")} +

    +
    +
    ${_("Updated")}: ${generated_at}
    +
    + +
    +
    +
    ${_("Total courses")}
    +
    ${courses_count}
    +
    ${_("Without test courses")}
    +
    +
    +
    ${_("Current issues")} ${current_year}
    +
    ${current_year_courses_count}
    +
    ${_("Used in the list below")}
    +
    +
    +
    ${_("Faculties")}
    +
    ${faculty_count}
    +
    ${_("With filled faculty field")}
    +
    +
    +
    ${_("Directions")}
    +
    ${directions_count}
    +
    ${_("With filled directions field")}
    +
    +
    + +
    + ${_("Languages")}: + % if language_summary: + % for language in language_summary: + ${language["label"]}: ${language["total"]} + % endfor + % else: + ${_("No data")} + % endif +
    + +
    +
    +
    +

    ${_("Courses by year")}

    +

    ${_("Course count dynamics by start date.")}

    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +

    ${_("Course Structure")}

    +

    ${_("Click a faculty or direction bar to filter the current courses below.")}

    +
    +
    +
    +
    +
    +
    +

    ${_("Courses by faculty")}

    +

    ${_("Top faculties by number of courses.")}

    +
    +
    +
    + +
    +
    + +
    +
    +
    +

    ${_("Courses by directions")}

    +

    ${_("Top directions by number of courses.")}

    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +

    ${_("All courses")}

    +

    ${_("Current courses with a start date in")} ${current_year} ${_("year")}.

    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + + +
    + + + + + + + + + + + + + +
    ${_("Course")}${_("Faculty")}${_("Direction")}${_("Language")}${_("Launches")}${_("Start")}
    +
    +
    ${_("There are no current courses for the selected filter in")} ${current_year} ${_("year")} + . +
    +
    +
    +
    +
    + + +<%block name="extrajs"> + + diff --git a/lms/templates/news/analyze_save.html b/lms/templates/news/analyze_save.html new file mode 100644 index 000000000000..5a9e69f3e813 --- /dev/null +++ b/lms/templates/news/analyze_save.html @@ -0,0 +1,775 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Analyze")} + +<%block name="extrahead"> + + + + +<%block name="content"> +
    +
    +
    +
    +
    Course analytics
    +

    ${_("Course analysis")}

    +

    + ${_("Summary of published courses without test organizations. The current courses of the current year are shown below; schedules of faculties and directions filter the list")}. +

    +
    +
    ${_("Updated")}: ${generated_at}
    +
    + +
    +
    +
    ${_("Total courses")}
    +
    ${courses_count}
    +
    ${_("Without test courses")}
    +
    +
    +
    ${_("Current issues")} ${current_year}
    +
    ${current_year_courses_count}
    +
    Используются в списке ниже
    +
    +
    +
    Факультетов
    +
    ${faculty_count}
    +
    С заполненным полем faculty
    +
    +
    +
    Направлений
    +
    ${directions_count}
    +
    С заполненным полем directions
    +
    +
    + +
    + Языки: + % if language_summary: + % for language in language_summary: + ${language["label"]}: ${language["total"]} + % endfor + % else: + Нет данных + % endif +
    + +
    +
    +
    +

    Курсы по годам

    +

    Динамика количества курсов по дате старта.

    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +

    Структура курсов

    +

    Нажмите на столбец факультета или направления, чтобы отфильтровать актуальные курсы ниже.

    +
    +
    +
    +
    +
    +
    +

    Курсы по факультетам

    +

    Топ факультетов по количеству курсов.

    +
    +
    +
    + +
    +
    +
    +
    +
    +

    Курсы по направлениям

    +

    Топ направлений по количеству курсов.

    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +

    Курсы топ 50

    +

    Актуальные курсы с датой старта в ${current_year} году.

    +
    +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + + + + + + + + + + + + +
    КурсФакультетНаправлениеЯзыкСтарт
    +
    +
    По выбранному фильтру нет актуальных курсов за ${current_year} год.
    +
    +
    +
    +
    + + +<%block name="extrajs"> + + diff --git a/lms/templates/news/course_details.html b/lms/templates/news/course_details.html new file mode 100644 index 000000000000..3d780bc1ca31 --- /dev/null +++ b/lms/templates/news/course_details.html @@ -0,0 +1,231 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%def name="pagetitle()">${_("Course analytics")} — ${course.display_name or course.id} + +<%block name="head_extra"> + + + +
    +
    + + +
    +

    ${_("Course analytics")}

    +

    ${course.display_name or course.id}

    +

    ${course.id}

    +
    +

    ${_("Selected runs")}: ${len(selected_runs)} / ${len(available_runs)} · ${_("Entire period")}

    + ${_("Go to course")} → +
    +
    + +
    +
    + ${_("Choose course runs")} +

    ${_("Select one or more runs, or view all runs for the entire lifetime of the course.")}

    +
    + +
    + ${_("Course runs")} + % for run in available_runs: +
    + + ${_("Only this run")} +
    + % endfor +
    +
    + + +
    +
    +
    +

    ${_("Selected runs")}: + ${', '.join(str(run.id.run) for run in selected_runs)} +

    +
    + +
    +
    +
    ${_("Total enrolled users")}
    +
    ${stats['total']}
    +

    ${_("Unique users across selected runs, including inactive enrollments.")}

    +
    +
    +
    ${_("Active enrollments")}
    +
    ${stats['active']}
    +

    ${_("Users currently enrolled in at least one selected run; does not measure learning activity.")}

    +
    +
    +
    ${_("Certificate recipients")}
    +
    ${stats['certificates']}
    +

    ${_("Users with an issued, available certificate.")}

    +
    +
    +
    ${_("Certificate rate")}
    +
    ${stats['certificate_rate']}%
    +

    ${_("Certificate recipients divided by all enrolled users.")}

    +
    +
    + +
    +
    +

    ${_("Enrollment history")}

    +

    ${_("Enrollment records across selected runs for the entire period. A user enrolled in two runs is counted twice. The current month is incomplete.")}

    + % if not history_total: +
    ${_("No new enrollments in this period.")}
    + % endif +
    +
      + % for month in enrollment_history: +
    1. +
      ${month['total']}
      + +
      ${month['label']}
      +
    2. + % endfor +
    +
    +
    + +
    +

    ${_("Enrollment status")}

    +

    ${_("Each user is counted once across selected runs. Inactive means no active enrollment in any selected run.")}

    +
    +
    ${_("Active")}
    ${stats['active']}
    +
    ${_("Inactive")}
    ${stats['inactive']}
    +
    ${_("New in the last 30 days")}
    ${stats['recent']}
    +
    ${_("Total enrollment records")}
    ${stats['enrollment_records']}
    +
    +
    +
    + +
    +
    +

    ${_("Learning outcomes")}

    +

    ${_("Based on saved grades and certificate records for enrolled users.")}

    +
    +
    ${_("Earned a passing grade at least once")}${stats['passed']}
    +

    ${_("A recorded passing date does not guarantee a currently valid certificate.")}

    +
    +
    +
    + ${_("Average saved grade")} + ${str(stats['average_grade']) + '%' if stats['average_grade'] is not None else '—'} +
    +

    ${_("Users with a saved grade")}: ${stats['grades_recorded']}. ${_("Missing grades are not counted as zero.")}

    +

    ${_("The average includes each saved grade in each selected run.")}

    +
    +
    +
    ${_("Certificate recipients")}${stats['certificates']}
    +

    ${_("Issued certificates across selected runs")}: ${stats['issued_certificates']}

    +

    ${_("Only issued, available certificates are counted. Revoked certificates and generation errors are excluded. This is not a download count.")}

    +
    +
    + +
    +

    ${_("Course information")}

    +

    ${_("Information for the displayed run")}: ${course.id.run}

    +
    +
    ${_("Organization")}
    ${course.org or '—'}
    +
    ${_("Faculty")}
    ${faculty or '—'}
    +
    ${_("Direction")}
    ${directions or '—'}
    +
    ${_("Language")}
    ${course.language or '—'}
    +
    ${_("Start date")}
    ${course.start.strftime('%d.%m.%Y') if course.start else '—'}
    +
    ${_("End date")}
    ${course.end.strftime('%d.%m.%Y') if course.end else '—'}
    +
    +
    +
    +
    +
    + +<%block name="extrajs"> + + diff --git a/lms/templates/news/detail.html b/lms/templates/news/detail.html new file mode 100644 index 000000000000..389e10840b91 --- /dev/null +++ b/lms/templates/news/detail.html @@ -0,0 +1,106 @@ +<%! from django.utils.html import escape %> + + + + + ${escape(news.title)} + + + +
    +
    +

    ${escape(news.title)}

    + ${news.created_at.date()} + + % if news.image: + ${escape(news.title)} + % endif + +
    ${news.content | n}
    +
    + + Назад к списку +
    + + diff --git a/lms/templates/news/form.html b/lms/templates/news/form.html new file mode 100644 index 000000000000..4bbcfda69a4d --- /dev/null +++ b/lms/templates/news/form.html @@ -0,0 +1,61 @@ + + + + Добавить новость + + + +

    Создание новости

    +
    + + +
    + + +
    + +
    + + +
    + +
    + + +
    + + +
    + Назад к списку + + diff --git a/lms/templates/news/list.html b/lms/templates/news/list.html new file mode 100644 index 000000000000..218569741f33 --- /dev/null +++ b/lms/templates/news/list.html @@ -0,0 +1,255 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("News")} + +<%block name="extrahead"> + + + +
    + + + % if news_list: +
    + % for news in news_list: +
    + % if news.image: + ${news.title} + % else: +
    + +
    + % endif + +
    + + +
    + ${news.created_at.date()} +
    + +
    + ${news.content[:100]}... +
    + + + ${_("Read more")} → + +
    +
    + % endfor +
    + % else: +
    +
    📰
    +

    ${_("No news available")}

    +

    ${_("Check back later for updates")}

    +
    + % endif +
    + +<%block name="extrajs"> + + diff --git a/lms/templates/static_templates/about.html b/lms/templates/static_templates/about.html index dae42e36dedb..128eef771e21 100644 --- a/lms/templates/static_templates/about.html +++ b/lms/templates/static_templates/about.html @@ -4,13 +4,549 @@ <%block name="pagetitle">${_("About")} -
    -
    -

    - <%block name="pageheader">${page_header or _("About")} -

    -

    - <%block name="pagecontent">${page_content or _("This page left intentionally blank. Feel free to add your own content.")} -

    + + + + + + + + + + +
    +
    +

    О нас - Open Kaznu

    +

    Казахский национальный университет им. аль-Фараби

    +
    -
    + +
    + +
    +
    +

    О платформе

    +
    + +
    +

    Казахский национальный университет им. аль-Фараби является лидером среди казахстанских университетов по внедрению MOOC на открытой платформе OpenEdx.

    + +

    С 2014-2015 учебного года Центр дистанционного образования КазНУ им. аль-Фараби совместно с преподавательским составом начал работу по созданию MOOC, и в настоящее время на http://open.kaznu.kz существует собственная платформа MOOC на основе системы Open edX.

    +
    + +
    +

    История развития

    +

    1 октября 2015 года были запущены первые открытые курсы от ведущих преподавателей КазНУ им. аль-Фараби - "Теория вероятностей" и "Физические задачи" с доцентом В. Кашкаровым, на которые записались около 250 и 500 студентов из разных регионов Казахстана соответственно.

    +
    + +
    +

    Эти курсы в основном посещали студенты 1-2 курсов КазНУ им. аль-Фараби, старшие классы Назарбаев интеллектуальных школ, профильные физико-математические и средние школы. Анализ данных показал большой интерес к этим курсам, что дает стимул преподавательскому составу и сотрудникам университета продолжать работу в этом направлении. К концу июня 2016 года в системе было зарегистрировано более двух тысяч слушателей.

    + +

    При активном содействии КазНУ им. аль-Фараби была запущена Национальная платформа открытого образования Казахстана (далее - НПОК).

    +
    +
    + + +
    +
    +

    Этапы развития

    +
    + +
    +
    +
    + 2014-2015 +

    Начало работы

    +

    Центр дистанционного образования КазНУ начинает работу по созданию MOOC

    +
    +
    + +
    +
    + Октябрь 2015 +

    Первый запуск

    +

    Запущены первые открытые курсы от ведущих преподавателей университета

    +
    +
    + +
    +
    + Январь 2016 +

    Реорганизация

    +

    Центр дистанционного образования преобразован в Институт дистанционного образования

    +
    +
    + +
    +
    + Февраль 2016 +

    Новый этап

    +

    Институт дистанционного образования преобразован в Институт новых образовательных технологий

    +
    +
    + +
    +
    + Июнь 2016 +

    Достижение

    +

    В системе зарегистрировано более двух тысяч слушателей

    +
    +
    +
    +
    + + +
    +
    +

    Институциональное развитие

    +

    В рамках реализации комплекса мер по развитию электронного обучения и организации работы НПОК для подготовки кадров на основе технологий дистанционного обучения (ДОТ) и усиления работы Центра дистанционного образования (ЦДО), с целью повышения мобильности предоставления образовательных услуг университета по новым образовательным программам, руководство университета приняло решение (приказ ректора №45 от 29.01.2016) о преобразовании структурного подразделения Центра дистанционного образования в Институт дистанционного образования (ИДО), в который вошел Центр массовых открытых онлайн-курсов (ЦМООК) в статусе проектного офиса национальной платформы.

    + +

    Были разработаны и утверждены вице-ректором по учебной работе Ахмет-Заки Д.Ж. Положение о ЦМООК, а также должностные инструкции руководителя, ведущего специалиста и специалиста Центра ЦМООК (от 14 марта 2016 г.).

    + +

    В феврале 2016 года Институт дистанционного образования был преобразован в Институт новых образовательных технологий, который продолжает работу ЦМООК.

    +
    +
    + + +
    +
    +

    Наша команда

    +
    + +
    +

    Руководство офиса

    +
    +
    +
    + +
    +

    Мусинова Асель

    +

    Заместитель директора Департамента по академическим вопросам

    +
    + +
    +
    + +
    +

    Круговых Илья

    +

    Руководитель Офиса академических и цифровых инноваций

    +
    +
    +
    + +
    +

    Специалисты open.kaznu.kz

    +
    +
    +
    + +
    +

    Арыстанова Айнур

    +

    Главный специалист

    +
    + +
    +
    + +
    +

    Канафина Диана

    +

    Ведущий специалист

    +
    + +
    +
    + +
    +

    Абылкасым Ерарыс

    +

    Умный-программист

    +
    +
    +
    + +
    +

    Специалисты dl.kaznu.kz

    +
    +
    +
    + +
    +

    Смагулова Шынар

    +

    Главный специалист

    +
    + +
    +
    + +
    +

    Малик Нурлы

    +

    Главный специалист

    +
    + +
    +
    + +
    +

    Жабаев Талгат

    +

    Инженер-программист

    +
    +
    +
    + +
    +

    Видеоинженер офиса

    +
    +
    +
    + +
    +

    Асылхан Аниятолла

    +

    Главный специалист

    +
    +
    +
    +
    +
    + + + + diff --git a/lms/templates/static_templates/author.html b/lms/templates/static_templates/author.html new file mode 100644 index 000000000000..7781c897fe65 --- /dev/null +++ b/lms/templates/static_templates/author.html @@ -0,0 +1,603 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("For authors")} + + + + + + + Создание онлайн-курса + + + + + + +
    +
    +

    ${_("Creating an Online Course for Authors")}

    +

    ${_("Al-Farabi Kazakh National University")}

    +
    +
    + +
    + +
    +
    +

    ${_("Methodological Requirements")}

    +
    + +
    +

    ${_("Requirements for Online Course Design")}

    + +
    +
    +
    + ${_("Electronic UMKD (e-UMKD)")} + +
    +
    +
      +
    • ${_("Electronic syllabus")}
    • +
    • ${_("Announcements for students – 1 per module")}
    • +
    • ${_("Questionnaires at the beginning and end of the course")}
    • +
    • ${_("Lecture notes with a list of sources – 1 per topic")}
    • +
    • ${_("Presentation – 1 per topic")}
    • +
    • ${_("Additional course materials with active links for online reading and viewing")}
    • +
    • ${_("Study questions – at least 10 per module")}
    • +
    • ${_("Discussion questions – at least 1 per module")}
    • +
    • ${_("Test tasks – at least 3 practice and 10 graded per module")}
    • +
    • ${_("Peer review assignments – no more than 1–2 per course")}
    • +
    +
    +
    + +
    +
    + ${_("Academic Presentation of the Online Course")} + +
    +
    +
      +
    • ${_("Measurable learning objectives")}
    • +
    • ${_("Expected Learning Outcomes (LO)")}
    • +
    • ${_("Indicators of LO achievement")}
    • +
    • ${_("Course language – student-oriented, accessible to a diverse audience")}
    • +
    • ${_("Course duration – at least 4 and no more than 15 weeks")}
    • +
    • ${_("Student workload per week – no less than 2 and no more than 5 hours")}
    • +
    +
    +
    +
    +
    +
    + + +
    +
    +

    ${_("Technical Requirements for Video")}

    +
    + +
    +

    ${_("Video recording must be carried out with professional equipment with track parameters not lower than the following:")}

    + +
    +
    +
    + ${_("Video")} + +
    +
    +

    ${_("Timing")}

    +
      +
    • ${_("Welcome/Promo video – no more than 3 minutes")}
    • +
    • ${_("Course introduction (purpose, objectives, structure, assignments) – 3 minutes")}
    • +
    • ${_("Video lecture – 3–9 minutes")}
    • +
    • ${_("Weekly video duration – at least 15 and no more than 30 minutes")}
    • +
    + +

    ${_("Technical Requirements")}

    +
      +
    • ${_("File format (container) – MP4")}
    • +
    • ${_("Codec – H264")}
    • +
    • ${_("Resolution: 1920 x 1080 (1080p)")}
    • +
    • ${_("Aspect ratio – 16:9")}
    • +
    • ${_("Frame rate – 25 or 30 fps")}
    • +
    • ${_("Progressive scan (25p/30p)")}
    • +
    • ${_("Bitrate – not less than 10,000 kbps and not more than 30,000 kbps")}
    • +
    +
    +
    + +
    +
    + ${_("Audio")} + +
    +
    +
      +
    • ${_("Codec: AAC, AC3, OGG, mp3")}
    • +
    • ${_("Channels: 2 (stereo)")}
    • +
    • ${_("Sample rate: 48 kHz")}
    • +
    • ${_("Audio stream: CBR not less than 192 kbps, VBR 160–320 kbps")}
    • +
    +
    +
    + +
    +
    + ${_("Audio Quality Characteristics")} + +
    +
    +
      +
    • ${_("Audio track must be true stereo, with the lecturer’s voice localized strictly between the left and right channels")}
    • +
    • ${_("Stereo track must be playable on monophonic equipment")}
    • +
    • ${_("Signal-to-noise ratio – at least 40 dB")}
    • +
    • ${_("Dynamic range of useful signal – no more than 16 dB")}
    • +
    • ${_("Average RMS loudness – from -14 dB to -12 dB")}
    • +
    • ${_("Peak loudness – limited to -2 dB")}
    • +
    +
    +
    + +
    +
    + ${_("Presentation Design Quality for Video")} + +
    +
    +
      +
    • ${_("Sans-serif font is recommended")}
    • +
    • ${_("No more than 2 fonts per course")}
    • +
    • ${_("Bullet points must be consistent throughout the course")}
    • +
    • ${_("Use contrasting text and background colors for readability")}
    • +
    • ${_("No more than 3 font colors per course")}
    • +
    • ${_("Avoid mixing contrasting colors within one sentence, paragraph, or table")}
    • +
    • ${_("Line thickness for borders, tables, and arrows should match font line thickness")}
    • +
    • ${_("Use photos, drawings, and animations in a consistent color scheme")}
    • +
    • ${_("Icons and infographics are recommended")}
    • +
    • ${_("Avoid merging same-sized scenes for natural perception")}
    • +
    • ${_("Avoid distracting clothing and makeup during recording")}
    • +
    • ${_("Slides: heading font – 20 pt, body text – 18 pt")}
    • +
    • ${_("Image size – at least 1280x720p (HD)")}
    • +
    • ${_("Image format – PNG")}
    • +
    • ${_("All external materials must have a CC Attribution license")}
    • +
    • ${_("Each borrowed image must include a source link")}
    • +
    +
    +
    +
    +
    +
    + + +
    +
    +

    ${_("Stages of Online Course Development")}

    +
    + +
    +
    +
    +
    + +
    +

    ${_("Application Submission")}

    +

    ${_("Submission of an application for online course development. Participation in the competition of pedagogical scenarios for creating and publishing online courses (MOOC/SPOC).")}

    +
    +
    + +
    +
    +
    + +
    +

    ${_("Course Development")}

    +

    ${_("Start of the online course development process.")}

    +
    +
    + +
    +
    +
    + +
    +

    ${_("Preparation of Materials")}

    +

    ${_("Preparation of online course materials by the author-developer. For documents on requirements and recommendations for recording video materials, please contact the Center for Massive Open Online Courses.")}

    +
    +
    + +
    +
    +
    + +
    +

    ${_("Material Expertise")}

    +

    ${_("Methodological and technical expertise of online course materials.")}

    +
    +
    + +
    +
    +
    + +
    +

    ${_("Video Recording")}

    +

    ${_("Recording of online course video materials.")}

    +
    +
    + +
    +
    +
    + +
    +

    ${_("Material Upload")}

    +

    ${_("Uploading online course materials to the platform.")}

    +
    +
    + +
    +
    +
    + +
    +

    ${_("Testing and Expertise")}

    +

    ${_("Beta testing and course expertise.")}

    +
    +
    +
    +
    + + +
    +

    ${_("Contacts")}

    +

    ${_("For all inquiries, please contact the Office of Academic and Digital Innovations: Building - Rectorate, Office 207, phone")} +7 727 377 33 30 ${_("(ext.")} 31-24, 11-33, 16-47)

    +
    +
    + + + + + + diff --git a/lms/templates/static_templates/catalog_transfer.html b/lms/templates/static_templates/catalog_transfer.html new file mode 100644 index 000000000000..c6ddbd95daf6 --- /dev/null +++ b/lms/templates/static_templates/catalog_transfer.html @@ -0,0 +1,1760 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Catalog Mook")} + + + + + Каталог МООК КазНУ + + + + + + +

    Каталог МООК КазНУ им. Аль-Фараби

    + +
    + + + + + + +
    + +
    + +
    +
    + + +
    +

    Methods of molecular biology

    +

    Тип курса: SPOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.0

    +

    Учебная нагрузка (кредиты): 0.5

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6B02207-востоковедения

    +

    Описание курса: ...

    +

    Осваиваемые навыки: ...

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Базовый восточный язык С1

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть + курс

    +

    Организация: Sungkyunkwan University

    +
    +
    +

    Arabic for Beginners: Communicating in Arabic Culture

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.0

    +

    Учебная нагрузка (кредиты): 0.5

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 7M022 Гуманитарные науки

    +

    Код и название образовательной программы: 7M03115-Регионоведение (восточные страны)

    + +

    Описание курса: Araic for Beginners: Communicating in Arabic Culture — курс Khalifa University + (государственный исследовательский университет, Абу-Даби, ОАЭ). + Курс является вторым из трёх в специализации по начальному арабскому языку. + Содержит погружение в лексику образования и школьной среды, полезен для общения + в международной рабочей среде, путешествий и финансовых ситуаций. + Подчёркивает важность коммуникации в технологически развитом глобальном обществе. + Особое внимание уделено пониманию частей тела в спортивном и медицинском контексте + и как они используются как физические и эмоциональные выражения в арабской культуре. + Основная информация: курс 2/3 в специализации, уровень Intermediate, 4 недели обучения по 2–3 часа в + неделю.

    + +

    Осваиваемые навыки: Повседневные фразы межличностного общения в личной и профессиональной среде.

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Восточный язык международного общения (Арабский)

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: Khalifa University (государственный исследовательский университет, Абу-Даби, ОАЭ)

    +
    +
    +

    Экономическое развитие Кореи

    +

    Тип курса: SPOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.0

    +

    Учебная нагрузка (кредиты): 0.5

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6B02207-востоковедения

    +

    Описание курса: Сформировать способность анализировать политические и социально-экономические положения + изучаемой страны для определения основных комплексных факторов современного исторического процесса.

    +

    Осваиваемые навыки: Дисциплина направлена на изучение теоретической и методологической основы факторов + исторических процессов; их взаимосвязи с современной политической модернизацией и экономическим + развитием, влияние на культурное изменение и общественно-политической жизни изучаемой страны + Востока.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Современные исторические процессы в изучаемой страны

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс +

    +

    Организация: Yonsei University

    +
    +
    +

    A Bridge to the World: Korean Language for Advanced I

    +

    Тип курса: SPOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.0

    +

    Учебная нагрузка (кредиты): 0.5

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6B02207-востоковедения

    +

    Описание курса: Цель дисциплины cформировать способность определять основную идею в незнакомом тексте; + применять в речи более сложные лексико-грамматические темы, конструкции, фразеологизмы, чем на + предыдущем уровне; осуществлять письменный перевод с соблюдением норм лексической эквивалентности.

    +

    Осваиваемые навыки: Дисциплина направлена на повышение культуры речи; пополнение лексического запаса при + изучении сложных текстов различной тематики; расширение профессионального кругозора.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Базовый восточный язык С1

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть + курс

    +

    Организация: Sungkyunkwan University

    +
    + +
    +

    Global Diplomacy – Diplomacy in the Modern World

    +

    Тип курса: SPOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.0

    +

    Учебная нагрузка (кредиты): 0.5

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6B02207-востоковедения

    +

    Описание курса: Цель дисциплины — сформировать способность понимать содержание понятий, подходов, + используемых мировой наукой в изучении роли глобализации. Курс формирует знания об основных трактовках + понятия глобализации, факторах влияния на глобальное развитие.

    +

    Осваиваемые навыки: Дисциплина направлена на изучение ключевых направлений влияния глобализации на + дипломатию в мировой политике, процесса внедрения глобальных тенденций в развитие международных + отношений.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Глобальная дипломатия в современном мире

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of London

    +
    +
    +

    Структурирование ценностей в современном Китае

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 28.91

    +

    Учебная нагрузка (кредиты): 0.964

    +

    Область образования: 8D02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6В02207-Востоковедение

    +

    Описание курса: Эта последовательность из четырех курсов предложит междисциплинарный подход к изучению + истории китайской культуры, рассматриваемой как последовательность режимов рациональности (философского, + бюрократического и экономического). Основное внимание будет уделено моментам смены парадигмы от одного + режима рациональности к другому. Для каждого из этих моментов культурные факты и артефакты — мысли, + литература, ритуалы — будут рассматриваться в связи с изменением социальных, политических и + экономических систем.

    +

    Осваиваемые навыки: Слушатели смогут рассматривать культурные ценности в связи с изменением социальных, + политических и экономических систем.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Внешняя политика изучаемой страны Востока

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть + курс

    +

    Организация: The Chinese University of Hong Kong

    +
    + +
    +

    Подходы социальных наук к изучению китайского общества Часть 1

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 15.66

    +

    Учебная нагрузка (кредиты): 0.522

    +

    Область образования: 8D02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6В02207-Востоковедение

    +

    Описание курса: Цель этого курса — превратить обучающихся в информированных потребителей исследований в + области социальных наук. Курс знакомит заинтересованных неспециалистов с концепциями, стандартами и + принципами проведения исследований в области социальных наук.

    +

    Осваиваемые навыки: могут оценивать доказательства и критически оценивать утверждения о важных социальных + явлениях

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Государственно-политический строй Китая

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: The Hong Kong University of Science and Technology

    +
    +
    +

    Successful Negotiation: Essential Strategies and Skills

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.0

    +

    Учебная нагрузка (кредиты): 5.0

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6B02207-Востоковедение

    +

    Описание курса: Successful Negotiation: Essential Strategies and Skills

    +

    Осваиваемые навыки: Этот модуль фокусируется на анализ переговоров, понимания стратегии и навыков ведения + переговоров

    +

    Язык курса: английский/русский

    +

    Дисциплины для перезачета: YaDEISV 4307 Язык дипломатии и этикета изучаемой стран Востока

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Michigan

    +
    + +
    +

    Management Foundations in the Hospitality Industry

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 30.0

    +

    Учебная нагрузка (кредиты): 2.0

    +

    Область образования: 8D11 Услуги

    +

    Область профессиональной подготовки: 8D111 Сфера обслуживания

    +

    Код и название образовательной программы: 8D11102 Туризм и гостеприимство

    +

    Описание курса: Этот курс представляет собой введение в мотивацию, лидерство, коммуникации, принятие + решений и руководство людьми посредством эффективного управления человеческими ресурсами (HR), этику, + социальную ответственность и управление потребительским опытом в индустрии гостеприимства путем изучения + основ менеджмента, ориентированного на обслуживание. Все необходимые материалы для чтения представлены в + модулях курса. Они включают внешние ссылки, статьи, графику и видео. Курс состоит из следующих + элементов: тематические видеоматериалы, обсуждения и тесты для проверки понимания.

    +

    Осваиваемые навыки: Понимать, как HR-отделы анализируют распределение ресурсов по рабочим местам и + факторы, влияющие на разработку рабочих мест; определять наиболее эффективные методы найма талантливых + сотрудников; применять лучшие практики при проведении собеседований; оценивать программу обучения и + социализации сотрудников; понимать оценку результатов работы; избегать юридической ответственности за + "небрежный прием на работу".

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Разработка инновационных решений для менеджмента в сфере туризма и + гостеприимства

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс +

    +

    Организация: University of North Texas

    +
    + +
    +

    Foundations of Hotel Budgeting and Forecasting

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 45.0

    +

    Учебная нагрузка (кредиты): 3.0

    +

    Область образования: 7M11 Услуги

    +

    Область профессиональной подготовки: 8D111 Сфера обслуживания

    +

    Код и название образовательной программы: 7M11104 - Ресторанное дело и гостиничный бизнес

    +

    Описание курса: Курс предназначен для обучения основным навыкам и знаниям бюджетирования и + прогнозирования в гостиничном бизнесе. Он предоставляет всестороннее понимание фундаментальных + концепций, практик и инструментов составления бюджета и прогнозирования, включая практические упражнения + и примеры из реальной жизни для применения знаний в индустрии гостеприимства.

    +

    Осваиваемые навыки: Составление бюджета отелей, оценка финансовых показателей, понимание тенденций в + отрасли, разработка стратегий на основе финансовых данных, практическое применение знаний в гостиничном + бизнесе.

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Прогнозирование и планирование в сфере гостеприимства

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: Starweaver

    +
    +
    +

    Introduction to Process Safety and Risk Analysis

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 13.01

    +

    Учебная нагрузка (кредиты): 0.434

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B052 Окружающая среда

    +

    Код и название образовательной программы: 6В05202 Экология

    +

    Описание курса: Каждый курс включает задание на рецензирование, в котором будут использованы знания, + полученные при изучении основополагающих концепций, аутентичных примеров из мировой и американской + практики, а также методов применения. Обратная связь в процессе экспертной оценки очень важна; учащимся, + а также нынешним и будущим специалистам в области общественного здравоохранения придется давать + критические отзывы.

    +

    Осваиваемые навыки: Environmental justice, Public Health

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Қоршаған ортаға әсерін бағалау және экологиялық сараптама

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of California, Davis

    +
    +
    +

    Impacts of the Environment on Global Public Health Specialization

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 115.0

    +

    Учебная нагрузка (кредиты): 3.85

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B052 Окружающая среда

    +

    Код и название образовательной программы: 6B11202- Экологическая инженерия

    +

    Описание курса: Каждый курс включает задание на рецензирование, в котором будут использованы знания, + полученные при изучении основополагающих концепций, аутентичных примеров из мировой и американской + практики, а также методов применения. Обратная связь в процессе экспертной оценки очень важна; учащимся, + а также нынешним и будущим специалистам в области общественного здравоохранения придется давать + критические отзывы.

    +

    Осваиваемые навыки: Environmental justice, Public Health

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Қоршаған ортаға әсерін бағалау және экологиялық сараптама

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of California, Davis

    +
    +
    +

    Экология: Динамика экосистем и их сохранение

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 13.01

    +

    Учебная нагрузка (кредиты): 0.434

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B052 Окружающая среда

    +

    Код и название образовательной программы: 6В05202 Экология

    +

    Описание курса: Этот курс представляет собой введение в экологию и динамику экосистем с использованием + системного мышления. На примере национального парка Горонгоза в Мозамбике слушатели изучат, как ученые + изучают экосистемы, и исследуют сложный комплекс факторов, которые лежат в основе управленческих усилий. + По окончании курса слушатели смогут решать реальные вопросы, связанные с охраной природы, например, + смогут ли экосистемы восстановиться после антропогенного воздействия и какую роль в этом восстановлении + могут и должны играть люди.

    +

    Осваиваемые навыки: В конце курса учащиеся смогут решать реальные вопросы охраны природы, например, может + ли экосистема восстановиться после антропогенного нарушения и какую роль люди могут и должны играть в + этом восстановлении.

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Экологический риск

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть + курс

    +

    Организация: American Museum of Natural History

    +
    +
    +

    Глобальный экологический менеджмент

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 18.79

    +

    Учебная нагрузка (кредиты): 0.626

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B052 Окружающая среда

    +

    Код и название образовательной программы: 6В05202 Экология

    +

    Описание курса: Узнайте о лучших экологических технологиях для устойчивого развития и о том, как ими + управляют в различных условиях по всему миру. Этот курс дает Вам возможность узнать о глобальных + тенденциях, влияющих на нашу окружающую среду и условия жизни, а также о том, как различные системы и + подходы к управлению окружающей средой, применяемые во всем мире, позволяют управлять ею. Сюда входят + современные технологии, созданные для охраны окружающей среды, и технологии устойчивого управления + почвой, методы защиты грунтовых вод и интегрированное управление водными ресурсами.

    +

    Осваиваемые навыки: Городское планирование, Развитие водных ресурсов, Природные ресурсы, Зеленые + технологии

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Стандарты менеджмента окружающей среды

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть + курс

    +

    Организация: Technical University of Denmark (DTU)

    +
    +
    +

    Введение в экологическое законодательство и политику

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.35

    +

    Учебная нагрузка (кредиты): 0.578

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B052 Окружающая среда

    +

    Код и название образовательной программы: 6B11202- Экологическая инженерия

    +

    Описание курса: Экологическое право может быть единственным институтом, стоящим между нами и планетарным + истощением...

    +

    Осваиваемые навыки: Экологическое право, Закон, Защита окружающей среды

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Экологическое законодательство и политика

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть + курс

    +

    Организация: The University of North Carolina at Chapel Hill

    +
    + +
    +

    Азиатские экологические гуманитарные науки: Ландшафты в переходный период

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 17.35

    +

    Учебная нагрузка (кредиты): 0.578

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B052 Окружающая среда

    +

    Код и название образовательной программы: 8D05205 Геоэкология и управление природопользованием

    +

    Описание курса: В этом курсе... (краткое описание можно расширить)

    +

    Осваиваемые навыки: анализ ландшафтов, экологическая гуманитаристика, устойчивость

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Техногенез и формирование природно-техногенных ландшафтов

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть + курс

    +

    Организация: University of Zurich

    +
    + + + + + + + + + +
    +

    Introduction to the Arctic: Climate

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 11.57

    +

    Учебная нагрузка (кредиты): 0.386

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 8D052 Окружающая среда

    +

    Код и название образовательной программы: 7М05204 - Метеорология

    +

    Описание курса: курс, посвященный изучению окружающей среды и климата циркумполярного Севера.

    +

    Осваиваемые навыки: Оценинть влияние Арктики на климат и экосистемы. Связь Арктики с остальным миром. Современное изменение климата, процессы, вызывающие его, и свидетельства его наличия в Арктике, а также последствия для быстро развивающегося Севера.

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Долгосрочные прогнозы погоды

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: The University of Alberta, the University of Tromso and the University of the Arctic

    +
    + +
    +

    Climate change education: sustainable environments

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 10.12

    +

    Учебная нагрузка (кредиты): 0.337

    +

    Область образования: 7M05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 8D052 Окружающая среда

    +

    Код и название образовательной программы: 6В05204 - Метеорология

    +

    Описание курса: В этом курсе мы изучаем компоненты ландшафтов и стихийные бедствия. Мы делаем акцент на действиях человека, которые влияют на окружающую среду, как с точки зрения изменения, так и сохранения. Кроме того, мы изучаем концепции, связанные с городами, мегаполисами и устойчивой средой. Курс также углубляется в связь между окружающей средой, здоровьем и болезнями»

    +

    Осваиваемые навыки: "Определение элементов, составляющие ландшафт, стихийные бедствия и действия человека, способствующие их изменению. Влияние городов, потребления и нашего экологического следа на окружающую среду. Влияние изменения климата на здоровье и действия, которые необходимо предпринять для создания более здоровой окружающей среды."

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Прогноз стихийных бедствий

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: Banco Interamericano de Desarrollo

    +
    + +
    +

    How Do We Manage Climate Change?

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 8.67

    +

    Учебная нагрузка (кредиты): 0.289

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 8D052 Окружающая среда

    +

    Код и название образовательной программы: 6В05204 - Метеорология

    +

    Описание курса: В этом курсе слушатели определят типы действий, которые мы можем предпринять для решения проблемы изменения климата. Эти действия делятся на две большие категории: 1) смягчение последствий, которое относится к усилиям по снижению выбросов парниковых газов или увеличению поглотителей углерода, и 2) адаптация, которая относится к нашей подготовке к климатическим воздействиям. Мы изучим технологии, программы и политику, связанные как со смягчением, так и с адаптацией. По окончании курса слушатели должны научиться определять и оценивать действия, предпринимаемые сообществами, правительствами и предприятиями в области климата.

    +

    Осваиваемые навыки: "Объясните общие цели политики смягчения последствий изменения климата и адаптации к ним Опишите основные источники и последние тенденции в области выбросов парниковых газов Объясните концепцию климатического риска и опишите его составляющие Примеры политики смягчения последствий и усилий по адаптации "

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Климат Казахстана, Климатология

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Colorado Boulder

    +
    + +
    +

    Act on Climate: Steps to Individual, Community, and Political Action

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 36.14

    +

    Учебная нагрузка (кредиты): 1.205

    +

    Область образования: 8D05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 8D052 Окружающая среда

    +

    Код и название образовательной программы: 8D05204 - Метеорология

    +

    Описание курса: Курс фокусируется на том, как перевести обучение в плоскость действий по борьбе с изменением климата в таких сферах, как продовольствие, энергетика, транспорт и построенная среда (города).

    +

    Осваиваемые навыки: В результате прохождения этого курса вы сможете: 1) Определить индивидуальные, общественные и политические действия, которые вы можете предпринять для эффективного решения проблемы изменения климата и реагирования на нее. 2) Описывать, как можно использовать достижения социальных наук для создания изменений на индивидуальном, общественном и политическом уровнях. 3) Чувствовать себя в состоянии продолжать влиять на то, как вы, ваше сообщество и политические лидеры решают проблемы изменения климата и реагируют на них. Используйте #UMichActonClimate в социальных сетях, чтобы делиться своими достижениями и общаться с другими учениками.

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Динамика климата

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Michigan

    +
    + +
    +

    Our Energy Future

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 52.05

    +

    Учебная нагрузка (кредиты): 1.735

    +

    Область образования: 8D05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 8D052 Окружающая среда

    +

    Код и название образовательной программы: 8D05204 - Метеорология

    +

    Описание курса: Этот курс предназначен для ознакомления студентов с проблемами энергетики в XXI веке, включая продовольствие и топливо, которые неразрывно связаны между собой. В рамках курса будут обсуждаться вопросы производства и использования энергии с точки зрения биологии, инженерии, экономики, климатологии и социальных наук. Этот курс будет посвящен современному производству и использованию энергии, а также последствиям этого использования, рассмотрению конечных запасов ископаемых энергоносителей, взаимосвязи продовольствия и энергии, воздействию на окружающую среду и климат, а также социальным и экономическим последствиям нашего современного производства и использования энергии и продовольствия.

    +

    Осваиваемые навыки: Вопросы производства и использования энергии с точки зрения биологии, инженерии, экономики, климатологии и социальных наук. Понимание современного производства и использованию энергии, а также последствия этого использования, рассмотрение конечных запасов ископаемых энергоносителей, взаимосвязи продовольствия и энергии, воздействию на окружающую среду и климат, а также социальные и экономические последствиям нашего современного производства и использования энергии и продовольствия.

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Доступный потенциал альтернативный источников энергии

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of California San Diego

    +
    + +
    +

    Remote Sensing Image Acquisition, Analysis and Applications

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 47.71

    +

    Учебная нагрузка (кредиты): 1.59

    +

    Область образования: 7M07 Инженерные, обрабатывающие и строительные отрасли

    +

    Область профессиональной подготовки: 7M073 Архитектура и строительство

    +

    Код и название образовательной программы: 7M07302-Геоинформатика

    +

    Описание курса: "Этот курс охватывает фундаментальную природу дистанционного зондирования, а также используемые платформы и типы датчиков. Он также обеспечивает углубленное изучение вычислительных алгоритмов, используемых для понимания изображений, начиная от самых ранних исторически важных методов до более современных подходов, основанных на глубоком обучении. Материал курса широко проиллюстрирован примерами и комментариями о том, как технология применяется на практике. Он подготовит участников к использованию материала в своих собственных дисциплинах и проведению более подробного изучения дистанционного зондирования и связанных тем."

    +

    Осваиваемые навыки: "Радарные системы Дистанционное зондирование Машинное обучение Анализ изображений"

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Дистанционное зондирование окружающей среды

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: UNSW Sydney (The University of New South Wales)

    +
    + +
    +

    Ақпаратты визуалдау: жетілдірілген әдістер

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 15.6

    +

    Учебная нагрузка (кредиты): 1.0

    +

    Область образования: 6B05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B052 Окружающая среда

    +

    Код и название образовательной программы: 6В05205-География

    +

    Описание курса: "Бұл курстың мақсаты – оқушыларды «Ақпаратты визуалдау: Негізгі қағидаттар» бөлімінде сипатталған негізгі диаграммалардан тыс визуалдаудың озық әдістерімен таныстыру. Берілген әдістер уақыт пен кеңістіктегі деректерді, желілер мен дарақтарды, сондай-ақ мәтіндік деректерді өңдеудің озық тәжірибелерін қамту мақсатында деректердің белгілі бір түрлеріне негізделген. Бұл модульде біз білім алушыларға D3.js-те инновациялық әдістерді қалай жасау керектігін үйретеміз. Оқу мақсаттары Мақсат: деректерді визуалдаудың әралуан түрлері үшін визуалдау шешімдерінің жобалық кеңістігін талдау. Берілген мәселені шешу үшін қандай құрастырымдар қолжетімді және олардың қандай артықшылықтары мен кемшіліктері бар екенін білу. - Уақыт - Кеңістік - Уақыт және кеңістік - Жүйе - Ағаш диаграммасы - Мәтін Бұл - «Ақпаратты визуалдау» мамандығы бойынша төртінші курс. Курс сізден бағдарламалау бойынша негізгі білім, сондай-ақ кейбір негізгі визуалдау дағдыларының (мамандандырудың бірінші курсында көрсетілген дағдыларға ұқсас) болуын талап етеді."

    +

    Осваиваемые навыки: Адамның компьютерлік өзара әрекеттесуі, кеңістіктік деректерді талдау, компьютерлік графика, интерактивті дизайн, сюжетті (графика), геовизуализация, статистикалық визуализация, деректерді визуализациялау

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Методы визуализации в географических исследованиях

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Flotida

    +
    + +
    +

    Sustainable Agricultural Land Management

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 11.8

    +

    Учебная нагрузка (кредиты): 0.7

    +

    Область образования: 6B07 Инженерные, обрабатывающие и строительные отрасли

    +

    Область профессиональной подготовки: 6B073 Архитектура и строительство

    +

    Код и название образовательной программы: 6В07303-Землеустройство

    +

    Описание курса: This course will cover the agricultural and urban water quality issues in Florida, their bases, land and nutrient management strategies, and the science and policy behind the best management practices (BMPs). Students will learn to evaluate BMP research and analyze its role in determining practices and policies that protect water quality.

    +

    Осваиваемые навыки: Resource, Sustainability, Agriculture, Nutrients

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Внутрихозяйственное и региональное землеустройство

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Florida

    +
    + +
    +

    A Scientific Approach to Innovation Management

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 18.79

    +

    Учебная нагрузка (кредиты): 0.626

    +

    Область образования: 8D05 Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 8D052 Окружающая среда

    +

    Код и название образовательной программы: 8D05202-География, 8D07303 - Землеустройство, 8D07305-Кадастр

    +

    Описание курса: How can innovators understand if their idea is worth developing and pursuing? In this course, we lay out a systematic process to make strategic decisions about innovative product or services that will help entrepreneurs, managers and innovators to avoid common pitfalls. We teach students to assess the feasibility of an innovative idea through problem-framing techniques and rigorous data analysis labelled ‘a scientific approach’. The course is highly interactive and includes exercises and real-world applications. We will also show the implications of a scientific approach to innovation management through a wide range of examples and case studies.

    +

    Осваиваемые навыки: Data Analysis, Decision Making, Probability & Statistics, Innovation, Leadership and Management

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Планирование диссертационного исследования и публикационной деятельности; Управление научными проектами в землеустройстве; Управление проектами пространственного развития

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: Università Bocconi

    +
    + +
    +

    Artificial Intelligence (AI) Education for Teachers

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 23.13

    +

    Учебная нагрузка (кредиты): 0.771

    +

    Область образования: 7M01 Педагогические науки

    +

    Область профессиональной подготовки: 7M014 Подготовка педагогов с предметной специализацией общего развития

    +

    Код и название образовательной программы: 7М01505-География

    +

    Описание курса: Сегодняшним учащимся необходимо знать, что такое искусственный интеллект (ИИ), как он работает, как использовать его в повседневной жизни и как он потенциально может быть использован в будущем. Использование ИИ требует навыков и ценностей, которые выходят далеко за рамки простого знания о программировании и технологиях. Этот курс разработан учителями для учителей и позволит преодолеть разрыв между общепринятыми представлениями об ИИ и тем, что это такое на самом деле. ИИ может быть внедрен во все области школьной программы, и этот курс покажет вам, как это сделать. \п\п Этот курс понравится учителям, которые хотят расширить свое общее понимание искусственного интеллекта, в том числе того, почему он важен для учащихся; и/или тем, кто хочет внедрить искусственный интеллект в свою преподавательскую практику и обучение своих учеников. Существует также уникальная возможность реализовать проект Capstone для студентов наряду с этим курсом профессионального обучения.Образовательная школа Маккуори при Университете Маккуори и IBM Australia совместно разработали этот курс, который соответствует австралийским профессиональным стандартам AITSL "Профессиональный уровень" на уровне AQF 8.

    +

    Осваиваемые навыки: "Машинное обучение Навыки мышления Дизайн-мышление Искусственный интеллект (AI)"

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Цифровые технологии в географическом образования

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: IBM, Macquarie University

    +
    + +
    +

    Writing Practice

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 52.047

    +

    Учебная нагрузка (кредиты): 1.735

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6B02211-Этнология и антропология

    +

    Описание курса: "Welcome to the Writing Practices course. This course aims to prepare you for performing writing tasks in academic contexts. In this course you will be introduced to basic academic writing skills. You will learn how the principles and structures of academic writing work when you are composing a variety of scripts. This course covers a wide variety of topics related to academic writing that will prepare you for presenting arguments, describing scientific processes and comparing objects and ideas. Course Positioning  This course is an intermediate level course in academic writing, intended for learners who have basic proficiency in reading and writing in English. The knowledge gained from this course will help you write effectively in most academic situations. System Requirements for the Course  You would need a functional computer, a steady internet connection, a good browser, and access to Google Drive for this course."

    +

    Осваиваемые навыки: Writing, Proofreading, Report Writing, Research, Writing and Editing, Editing

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Scientific writing

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: Birla Institute of Technology & Science, Pilani

    +
    + +
    +

    Анатомия верхних и нижних конечностей

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 21.686

    +

    Учебная нагрузка (кредиты): 0.723

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6B02210-Археология

    +

    Описание курса: Этот курс состоит из двух основных частей, одна из которых посвящена нижней конечности, а другая - верхней. Мы покажем, как различные системы, снабжающие и организующие конечность, контролируют ее функцию. Предусмотрен набор вводных лекций, которые позволят более опытным студентам освежить свои знания о конечностях и послужат руководством для тех, кто имеет меньший опыт. За этими лекциями последуют подробные разборы конечностей с акцентом на локомоции для нижних конечностей и на положение и функции рук для верхних конечностей. Этот курс является частью 2/4 в рамках специализации Йельского университета по анатомии человека.

    +

    Осваиваемые навыки: Anatomy

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Основы физической антропологии

    +

    Учебная нагрузка в кредитах: 6.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: Yale University

    +
    + +
    +

    Цифрлық дәуірдің интеллектуалдық құралдары

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 6.024

    +

    Учебная нагрузка (кредиты): 0.201

    +

    Область образования: 6B03 Социальные науки, журналистика и информация

    +

    Область профессиональной подготовки: 6B032 Журналистика и информация

    +

    Код и название образовательной программы: 6В03208-Цифровое архивоведение и документоведение

    +

    Описание курса: "Цифрлық технологиялар бүкіл әлемдегі бизнес, үкімет және қоғамды өзгерту үстінде. Олардың әлем экономикасының әрбір секторы үшін жаңа тәуекелдер мен мүмкіндіктер ашатыны анық. Мұндай “футурология” жалпы көріністі ұсына алады, ал сіз үшін цифрлық әлем нені білдіреді? Болашақтағы жасанды интеллект пен big data-ға негізделген, автоматтандырылған жұмысқа дайынсыз ба? Бұл курс осы сұрақтарға бірегей көзқараспен жауап береді: сізге интеллектуалдық анализ әлемін таныстырады. Ең алдымен, осы курста цифрлық дәуірде жоғары бағаланатын кейбір дағдылар айтылып, олардың неліктен құнды болатыны түсіндіріледі. Сондай-ақ курс цифрлық әлемді түсінуде когнитивтік артықшылық беру үшін сізге АҚШ интеллектуалдық қауымдастығы қалыптастырған менталдық модельдер мен практикалық құрылымдарды ұсынады. Бұл артықшылық тыңшылық пен құпияларға қатысты емес. Ойлау жүйесін жақсартуға қатысты. Бұл аналитикалық құралдар жинағы MBA стиліндегі стратегиялардың қажыған, статикалық құрылымдарынан мүлде бөлек. Оның орнына, бұл құралдар сізге цифрлық дәуірдегі құбылмалылық, белгісіздік, екіұштылық және алдампаздықты еңсеруге көмектеседі. Түптеп келгенде, осы курс әлем цифрландырылған сайын, келешектегі жетістік өзіңізге де, басқаларға да сұрақтарды жақсырақ қоюға көбірек тәуелді екенін дәлелдейді. Осылайша, ол сізді интеллектуалдық талдаушыға тән сұрақ қоюға негізделген ойлау жүйесін қабылдауға үйретеді. Қысқасы, әлдеқайда анық, құрылымдалған ойлау жүйесінің арқасында тұрақты бизнес артықшылыққа қол жеткізу үшін осы курсқа қатысыңыз."

    +

    Осваиваемые навыки: Зерттеу және дизайн, бизнес талдау, деректерді басқару, іскерлік интеллект, сыни ойлау, стратегия және операциялар, коммуникация, көшбасшылық және басқару, процестерді талдау, мәдениет, бейімделу, іскерлік психология, эмоционалдық интеллект, кәсіпкерлік, болжау, ынтымақтастық, инновация, ықтималдық және статистика , Шешім қабылдау, Деректерді талдау, Адам ресурстары, Маркетинг, Сатылым, Стратегия, Талдау, Адамның компьютерлік өзара әрекеттесуі, Адамдарды талдау

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Офисные информационные технологии

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: IE university

    +
    + +
    +

    Introduction to Ancient Egypt and Its Civilization

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 11.566

    +

    Учебная нагрузка (кредиты): 0.386

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6В02212-История со знанием ин.яз.

    +

    Описание курса: Колоссальные пирамиды, величественные храмы, золотые сокровища, загадочные иероглифы, могущественные фараоны, странные боги и таинственные мумии - особенности культуры Древнего Египта, которые восхищали людей на протяжении тысячелетий. В Библии упоминаются ее боги, правители и пирамиды. Соседние культуры древнего Ближнего Востока и Средиземноморья писали о его богоподобных царях и кажущихся бесконечными запасах золота. Греки и римляне описывали отдельные аспекты культуры и истории Египта. В начале 19-го века наполеоновская кампания в Египте привлекла внимание к чудесам этой древней страны, и интерес общественности резко возрос. Вскоре после этого Шампольон расшифровал египетские иероглифы и открыл другим ученым путь к тому, что египетские тексты касались медицины, стоматологии, ветеринарной практики, математики, литературы, бухгалтерского учета и многих других тем. Затем, в начале 20-го века, Говард Картер обнаружил гробницу Тутанхамона и ее сказочное содержимое. Выставки этих сокровищ несколько десятилетий спустя привели к первому в мире блокбастеру, а их возрождение в 21 веке поддерживает интерес к ним. Присоединяйтесь к д-ру Дэвиду Сильверману, профессору египтологии в Пенне, куратору египетского отдела Пеннского музея и куратору выставок Тутанхамона, чтобы совершить экскурсию по загадкам и чудесам этой древней земли. Он разработал этот онлайновый курс и расположил его в галереях всемирно известного музея Пенна. Он использует множество оригинальных египетских артефактов для иллюстрации своих лекций, направляя студентов в процессе их самостоятельного знакомства с этой увлекательной культурой.

    +

    Осваиваемые навыки: знать особенности культуры Древнего Египта; уметь анализировать первоисточники по культуре и истории Египта. ; уметь оценивать отдельные аспекты культуры и истории Египта; ознакомиться со множествами оригинальными египетскими артефактами.

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Цивилизация древнего Востока

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Pennsylvania

    +
    + +
    +

    Academic Information Seeking

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 8.675

    +

    Учебная нагрузка (кредиты): 0.289

    +

    Область образования: 6B03 Социальные науки, журналистика и информация

    +

    Область профессиональной подготовки: 6B032 Журналистика и информация

    +

    Код и название образовательной программы: 6В03208-Цифровое архивоведение и документоведение

    +

    Описание курса: "Этот курс познакомит Вас с основными элементами поиска академической информации - мы изучим процесс поиска, начиная с определения стратегии и заканчивая оценкой и документированием результатов поиска. Посещение этого курса сделает Вас опытным искателем информации. Вы научитесь проводить всесторонний поиск литературы на основе Вашего собственного исследовательского задания. Вы пройдете через различные этапы поиска информации, начиная с выбора соответствующих стратегий и методов поиска и заканчивая оценкой результатов поиска, документированием процесса поиска и цитированием своих источников"

    +

    Осваиваемые навыки: "Этот курс познакомит Вас с основными элементами поиска академической информации - мы изучим процесс поиска, начиная с определения стратегии и заканчивая оценкой и документированием результатов поиска. Посещение этого курса сделает Вас опытным искателем информации. Вы научитесь проводить всесторонний поиск литературы на основе Вашего собственного исследовательского задания. Вы пройдете через различные этапы поиска информации, начиная с выбора соответствующих стратегий и методов поиска и заканчивая оценкой результатов поиска, документированием процесса поиска и цитированием своих источников. Посещение курса позволит Вам: - Определить свою информационную потребность - Оценить базы данных и другие информационные ресурсы - Определить стратегию поиска и использовать различные методы поиска - Сформулировать поисковые строки на основе собственного исследовательского задания - Определить релевантные типы материалов - Провести критическую оценку источников - Более эффективно искать в Интернете - Избегать плагиата - Правильно цитировать - Работать со справочным аппаратом - Документировать процесс поиска"

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Информационная безопасность и защита информации

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Copenhagen

    +
    + +
    +

    Community Organizing for Social Justice

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 11.566

    +

    Учебная нагрузка (кредиты): 0.386

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6В02205-История

    +

    Описание курса: В курсе рассматриваются конкретные стратегии организации сообщества для достижения социальной справедливости в многообразном демократическом обществе. В курсе рассматриваются основные концепции социальной справедливости и практические шаги по объединению людей для определения целей и приоритетов, оценки активов и потребностей сообщества, разработки планов действий и создания поддержки для их реализации. Курс включает информацию о том, как формировать основные группы, наращивать организационный потенциал и формулировать стратегию работы с избирателями и союзниками. Поскольку каждый человек учится по-своему, в курсе представлены разнообразные учебные задания, информация об успешных программах, индивидуальные инструкции и упражнения для малых групп, а также практические материалы для решения проблем и планирования программ. Вы можете использовать эти упражнения с отдельными людьми, с небольшой группой людей или с несколькими группами в рамках общественной кампании. Курс основан на работе с людьми, которые стремятся к изменениям в обществе в столичном Детройте - районе, который становится как более сегрегированным, так и более разнообразным. Но он предназначен и для тех, кто хочет создавать изменения в сообществах повсюду. Если Вы пройдете курс с идеями в голове и закончите его с планами в руках, то наша цель будет достигнута.

    +

    Осваиваемые навыки: Advocacy, Community Organizing, Cultural Sensitivity, Diversity Awareness, Cultural Responsiveness, Communication Strategies, Social Sciences, Interpersonal Communications, Social Justice

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Историческая политика

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Michigan

    +
    + +
    +

    Basic Statistics

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 39.036

    +

    Учебная нагрузка (кредиты): 1.301

    +

    Область образования: 6B02 Искусство и гуманитарные науки

    +

    Область профессиональной подготовки: 6B022 Гуманитарные науки

    +

    Код и название образовательной программы: 6В02205-История

    +

    Описание курса: "Понимание статистики необходимо для понимания исследований в области социальных и поведенческих наук. В этом курсе Вы изучите основы статистики; не только то, как ее вычислять, но и то, как ее оценивать. Этот курс также подготовит Вас к следующему курсу специализации - курсу ""Инференциальная статистика"". В первой части курса мы обсудим методы описательной статистики. Вы узнаете, что такое случаи и переменные и как можно вычислить показатели центральной тенденции (среднее, медиана и мода) и дисперсии (стандартное отклонение и дисперсия). Далее мы обсудим, как оценивать взаимосвязи между переменными, и введем понятия корреляции и регрессии. Вторая часть курса посвящена основам вероятности: вычислению вероятностей, распределению вероятностей и выборочным распределениям. Вам необходимо знать об этих вещах для того, чтобы понять, как работает инференциальная статистика. Третья часть курса состоит из введения в методы инференциальной статистики - методы, которые помогают нам решить, достаточно ли сильны закономерности, которые мы видим в наших данных, чтобы делать выводы об интересующей нас основной совокупности. Мы обсудим доверительные интервалы и тесты на значимость. Вы не только узнаете обо всех этих статистических концепциях, но и будете обучены самостоятельно рассчитывать и генерировать эти статистические данные с помощью свободно распространяемого статистического программного обеспечения."

    +

    Осваиваемые навыки: Statistical Analysis, Probability & Statistics, Statistics, Statistical Methods, Data Analysis, Statistical Inference, Data Science, Applied Mathematics, Probability, Mathematics and Mathematical Modeling, Statistical Modeling, Correlation Analysis, Statistical Hypothesis Testing, Probability Distribution, Data Collection, Mathematical Modeling, Sampling (Statistics), Regression Analysis, Descriptive Statistics, Exploratory Data Analysis, Sample Size Determination, Analytics

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Клиометрика

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: University of Amsterdam

    +
    + +
    +

    Культурная антропология

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: 6B03 Социальные науки, журналистика и информация

    +

    Область профессиональной подготовки: 6В03102-Культурология

    +

    Код и название образовательной программы: 6В03102-Культурология

    +

    Описание курса: Курс изучает элементы, которые объединяют нас – людей – как вид, а также разнообразие и особенности нашего поведения. Этот курс анализирует институты, найденные в разных культурах, таких как религия, экономика, политика, право, семья, их развитие вместе с современным миром, так что теперь в центре анализа находятся и такие феномены, как корпоративные структуры, научные организации, социальные сети и проч. Курс также ставит современную культурную антропологию в исторический контекст, устанавливая ее происхождение и отслеживая ее развитие на протяжении многих лет. Короче говоря, курс направлен на то, чтобы дать слушателю оценку многообразия человеческого опыта и важнейшие инструменты, необходимые для исследования культурных аспектов.

    +

    Осваиваемые навыки: "Владеть базовым категориально-понятийным аппаратом дисциплины, связанным с изучением культурных форм, процессов и практик; Анализировать социокультурные, этнополитические, этносоциальные факторы исторического развития мирового, национальных, региональных, локальных сообществ, антропогенеза, происхождения и эволюцию человека; Характеризовать, классифицировать и систематизировать актуальные вопросы основных культурантропологических школ и направлений; Владеть методами и техниками культурантропологических исследований, сложившихся в рамках различных школ и направлений; Применять навыки культурологического анализа при исследовании различных социокультурных практик в широком социальном и историческом контексте"

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Культурная антропология

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Sociology

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: The course aims to develop skills of evaluating programs for solving social problems based on sociological imagination, critical thinking and sociological research methods to analyze and interpret social processes and institutions. The course is based on the study of theories of sociology associated with the names of key foreign and domestic scholars who have contributed to the development of sociology. Sociology studies a variety of topics from crime to religion, from the family to the state, divisions of race and social class to the shared beliefs of a common culture, from social stability to radical change in whole societies. Basically, everything that surrounds us is a subject of sociology. By discovering theoretical concepts, research methods and analyzing the processes as sociologists, students will be able to find answers these kind of questions: Are we free or do we follow the crowd? Why there is inequality? How people and institutions which make up society interact? What are the things that influence your life (family background, ethnicity, social class, religion, gender)? Why there is war? This is facilitated by the methodology of the course: special situational tasks, interesting games, watching movies, videos, test tasks and etc.

    +

    Осваиваемые навыки: "Тo explain the categories of sociology, trends in the development of society on the basis of sociological macro- and micro-theories and concepts. Тo interpret social reality based on sociological imagination and critical thinking. Тo apply sociological methods and theoretical constructs to develop research design and analyze specific social problems. Тo analyze the features and interrelation of social processes (socio-economic, political, cultural) and social institutions from the position of a sociological perspective and the value system of Kazakhstani society. Тo summarize information on trends in the development of social structures, individual and family, economy, education, culture, religion, social communications and globalization based on comparative research. Тo evaluate programs for solving social problems and situations in own professional fields based on socio-ethical values."

    +

    Язык курса: английский

    +

    Дисциплины для перезачета: Sociology

    +

    Учебная нагрузка в кредитах: 2.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Философия

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: "Целью курса является формирование у студентов способности самостоятельно выявлять, систематизировать и оценивать мировоззренческие компоненты изучения природного и социального мира. Это - подробный и увлекательный экскурс в интеллектуальную историю современной цивилизации. Каждый слушатель сможет познакомиться с классическими и современными проблемами философии (например, философия сознания, проблемы искусственного интеллекта, современной космологии и др.) и выбрать темы, которые интересны именно ему в рамках выбранной образовательной программы. Курс относится к циклу общеобразовательных дисциплин и предназначен для бакалавров любых направлений подготовки. Курс читается кафедрой философии КазНУ им. аль-Фараби – ведущим научным и образовательным центром, аккумулирующим традиции и инновации казахстанской философской школы."

    +

    Осваиваемые навыки: описывать основное содержание онтологии и метафизики в контексте исторического развития философии; классифицировать методы научного и философского познания мира;обосновывать мировоззрение как продукт философского осмысления и изучения природного и социального мира на основе понимания специфики философского мышления; интерпретировать содержание и специфические особенности мифологического, религиозного и научного мировоззрения; аргументировать собственную нравственную позицию по отношению к актуальным проблемам современного глобального общества, социально-культурных и личностных ситуаций для обоснования и принятия этических решений; самостоятельно провести анализ философского и аксиологического содержание проблем в профессиональной области для презентации и обсуждения результатов исследований.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Философия

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Педагогика высшей школы

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: Если вы хотите влиять на людей и заинтересованы в развитии своих навыков как педагога, методиста, исследователя и ученого. Тогда Вам нужно пройти курс «Педагогика высшей школы». Потребность в великих преподавателях – насущная потребность . Изучив курс, вы сможете влиять непосредственно на качество образования, на доступность обучения. как преподаватель научитесь лучше поддерживать широкий и разнообразный контингент студентов, овладеете успешными практиками организации воспитания и эффективного самоменеджмента.

    +

    Осваиваемые навыки: "Объяснять современную стратегию развития высшего образования в Казахстане на основе осмысления парадигм в мировом образовательном пространстве; Изучать уровень усвоения обучающимися содержания образования, исследовать образовательную среду. Самостоятельно планировать и проводить семинарские, практические, лабораторные занятия с учетом требований разработанных и утвержденных методических указаний. . Строить воспитательный процесс с учетом национальных приоритетов Казахстана и приобщать обучающихся к системе социальных ценностей. Разрабатывать учебно-методические материалы для сопровождения образовательного процесса и реализации инноваций в обучении и воспитании обучающихся."

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Педагогика высшей школы

    +

    Учебная нагрузка в кредитах: 0.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Конфликтология

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: 6B03 Социальные науки, журналистика и информация

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: 6В03106-Политология

    +

    Описание курса: Курс “Конфликтология” предназначен для учащихся выпускных классов средних учебных заведений, желающих поступать на учебу на специальность социология, социальная работа, политология КазНУ им. аль-Фараби. Также он будет полезен выпускникам школ, которые решили поступать на другие факультеты, где изучаются социальные процессы, межличностные коммуникации. Также данный курс полезен для всех желающих изучить природу, причины, типологию , управление и разрешение конфликтных ситуаций . Курс “Конфликтология ” даст комплексные знания по конфликтным ситуациям и способам их разрешения. Конфликты являются неизбежным спутником социальной жизни, определяются природой человека. Современному человеку необходимо научиться снижать уровень конфликтного противостояния, используя классические и современные достижения в области управления и профилактики конфликтов. Важно уметь правильно выявлять их причины, управлять их протеканием и разрешением, снижать уровень конфликтного противостояния. Поэтому представляет научный и практический интерес изучение основ конфликтологии, классических исследований и современных достижений в области развития конфликтологии.

    +

    Осваиваемые навыки: Skills not specified

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Политическая конфликтология

    +

    Учебная нагрузка в кредитах: 0.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Организация научных исследований

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: Просто, доступно, интересно о нормативном знании в научном исследовании. Практические советы, рекомендации опытных людей от науки. Выбор вуза, руководителя, темы исследования. Структура работы и контрольные элементы диссертации. Варианты написания и требования к работе. Научные публикации, научный этикет, научный стиль.

    +

    Осваиваемые навыки: Skills not specified

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Организация научных исследований

    +

    Учебная нагрузка в кредитах: 0.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Мәдениеттану

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: «Мәдениеттану» курсы елімізде қабылданған «Рухани жаңғыру» бағдарламасы бойынша міндетті пәндердің қатарына енеді. Курс студенттерге осы пәнді оқытуда озық әлемдік және отандық оқыту тәжірибесіне негізделеді. Онлайн курста авторлық дәрістермен қатар қосымша материалдар, слайдтар, тапсырмалар, оқыған материалды пысықтайтын тестілер және қорытынды емтихан тестілері келтірілген. Курсты игеру нәтижесінде студенттер қазіргі мәдениеттанудың негізгі ұғымдары, категориялары және қағидаттарымен, әлемдік мәдениеттер және өркениеттердің басты құндылықтарымен, Қазақстан мәдениетіндегі тарихи және заманауи үдерістермен таныса алады.

    +

    Осваиваемые навыки: "Мәдениеттану саласындағы негізгі категориялық-түсініктік аппаратты қолдану. Әр түрлі кезеңдердегі мәдениеттің тарихи типтерінің негізгі сипаттамаларын талдау. Мәдениет дамуының әр түрлі деректеріне әлеуметтік-мәдени сараптама жүргізу. Әлеуметтік-мәдени сипаттағы мәселелерге жеке ұстанымын қалыптастыру және негіздеу. Мәдениет жетістіктерін олардың жасалуының тарихи мәнмәтінін түсіну негізінде бағалау."

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Мәдениеттану

    +

    Учебная нагрузка в кредитах: 2.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Культурология

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: В процессе изучения курса «Культурология» вы встретитесь с многочисленными точками зрения, взаимодополняющими друг друга оценками одних и тех же явлений. Ваша самостоятельность и активность должны проявиться в том, чтобы, ознакомившись с разными позициями, определиться в своих предпочтениях, совершенствовать и развивать свои способности. Представленный курс объединяет сильные стороны социальных и гуманитарных наук, опирается на методы и теории истории, философии, социологии, литературоведения и коммуникационных исследований. Творческие задания курса способствуют включению слушателя в практическую реализацию мира культуры.

    +

    Осваиваемые навыки: объяснять базовый категориально-понятийный аппарат на основе рефлексивного усвоения ценностей и достижений культуры; упорядочивать характеристики исторических типов культуры на основе генезиса основных культурных явлений; проводить социокультурный анализ практик культуры для осмысления культуротворческой деятельности современного казахстанского общества; обобщать результаты исследований достижений отечественной культуры, ее современные проблемы и перспективы развития на основе понимания их создания для толерантного восприятия социальных, этнических, конфессиональных и культурных различий; оценивать развитие мировой культуры для понимания богатства культурного наследия.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Культурология

    +

    Учебная нагрузка в кредитах: 2.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Жоғары мектеп педагогикасы

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: Болашақ жоғары мектеп оқытушылары үшін «Қазіргі жоғары білім беру» мәселелерін жан-жақты қарастырып, университеттің оқу-тәрбие үдерісін қалай ұйымдастыру, пәннің оқу-әдістемелік кешенін құрастыру, дәстүрлі және инновациялық оқыту әдістерін, жаңа білім беру технологияларын тиімді пайдалануды, сонымен бірге куратор-эдвайзерлік қызметті ұйымдастыруды, тәрбиелік іс-шараларды өткізу сценарийлерін жасауды, педагогикалық қарым-қатынас құралдарын, басқару стилдерін нәтижелі қолдануға, білім беру саласындағы реформаларды талдап, даму тенденцияларын саралап, оқып үйренуге мүмкіндік береді.

    +

    Осваиваемые навыки: "заманауи жоғары кәсіби білім берудің даму сатыларын, әдіснамалық аппараттың параметрлері мен әдіснамалық деңгейлерін білу; Қазақстанда жоғары кәсіби білім берудің жүйесіне талдау жасай білу; жоғары мектеп оқытушысының кәсіби-педагогикалық мәдениет мен құзіреттілігі негіздерін игеру; Білім берудің TLA-стратегиясын, ЖОО-да кредиттік жүйемен білім беру бойынша студенттердің өзіндік жұмысын жобалау; заманауи дидактикалық принциптер мен талдау технологиясын білу, білім беру мен тәрбиелеудің технологиясы; Жоғары кәсіби білім берудің мазмұнын құрастыру; Блум таксономиясы бойынша құзіреттілікті бағалау; жоғары мектепте дәстүрлі және инновациялық әдістер мен білім беруді ұйымдастырудың жаңа технологияларын қолдану. ЖОО-да білім беру процесінде студенттер мен оқытушылар арасындағы коммуникативті қарым-қатынас технологиясын бағалау және білім алушыларды ынталандыру. "

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Жоғары мектеп педагогикасы

    +

    Учебная нагрузка в кредитах: 0.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Философия

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: «Философия» пәні болашақ мамандардың бойында тәуелсіз сыни ойлау түсінігін қалыптастыру мен маңызды дүниетанымдық ұғымдарды түсінуге бағытталған. Пәнді оқу барысында келесі аспектілер қарастырылады: Философияның пайда болуы мен дамуы. Философияның пәні мен әдістері. Философияның тарихи типтері. Әлемді философиялық түсінудің негізі. Болмыс мәселесі. Онтология және метафизика. Сана мен тіл. Таным мен шығармашылық. Ғылыми және ғылыми емес білім. Ғылым мен техника. Адам философиясы және құндылықтар әлемі. Өмір мен өлім. Өмірдің мәні. Этика. Құндылықтар философиясы. Еркіндік. Эстетика. Қабылдау және әсемдікті жасау. Қоғам мен мәдениет. Тарих философиясы. «Мәңгілік Ел» және «Рухани жаңғыру» – Қазақстанның жаңа философиясы.

    +

    Осваиваемые навыки: Пәнді оқытудың нәтижесінде студенттер мынадай қабілеттерге ие болады: философияның тарихи даму шеңберінде метафизика мен онтологияның негізгі мазмұнын сипаттау; философиялық ойлау тұрғысындағы шындықтың ерекшеліктерін түсіндіру; дүниені танудың философиялық және ғылыми әдістерін жүйелеу; қазіргі дүниедегі адамның әлеуметтік және жеке басының құндылықтары ретінде негізгі дүниетанымдық ұғымдардың рөлі мен маңыздылығын негіздеу; кəсіби саладағы мəселелердің философиялық мазмұнын анықтауға жəне талқылауға арналған нəтижелерді ұсынуға байланысты зерттеулер жүргізу және т.б.

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Философия

    +

    Учебная нагрузка в кредитах: 0.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Религиозная философия

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: 6B02203 Религиоведение

    +

    Описание курса: Курс приобщает к достижениям духовной культуры человечества, способствует формированию собственной культуры мышления, развивает и укрепляет ценностные ориентации мировой культуры (терпимости к другим и другому), позволяет понять свое место в мире, найти смысл жизни, обрести и реализовать свое предназначение.

    +

    Осваиваемые навыки: В упрощенной форме изучение терминологического аппарата классической и современной философии и ознакомление с основными философскими школами; ознакомление с основными проблемами научных и религиозных картин мира, человеческого знания и особенностей его проявления в современном обществе, соотношение духовных и материальных ценностей, их роли в жизнедеятельности человека, общества, цивилизации.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Религиозная антропология

    +

    Учебная нагрузка в кредитах: 6.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Саясаттану

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: Бұл курс қоғамдағы саясаттың табиғатын, оның орны мен рөлін түсініп, саясаттың қыр-сырын білгісі келетін кез келген жастағы білім алушыларға арналған. Бұл пән қоғамның саяси саласы туралы жүйелі білім беріп, азамат ретінде саяси үрдістер мен институттар жұмысына қатысуға және бағалауға, талдау мен болжауға қажетті дағдыларды қалыптастырады.

    +

    Осваиваемые навыки: "негізгі саяси ұғымдарды, қоғам мен оның кіші жүйелерін зерттеуге арналған теориялар мен тәсілдерді меңгеру арқылы саяси саланы зерделеу және бағалау. оқытылатын пәннің аясында ғылыми ой мен теория мазмұны негізінде түрлі қоғамдық салалардағы қарым-қатынастың жағдайын түсіндіру. қазіргі қоғамның және оның саяси институттарының жұмыс істеу ерекшеліктері мен негізгі қағидаттарын меңгеру арқылы оларды талдау, ұсыныстар беру және келешегін болжау. заманауи қоғамның саяси мәселелерін сипаттау мен талдау арқылы саяси процестер мен қатынастардың өзара байланыстарын ашып беру. қазақстандық қоғамның даму үрдістері мен жаңғыру ерекшеліктерін талдау негізінде ондағы саяси институттардың рөліне баға беру."

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Саясаттану

    +

    Учебная нагрузка в кредитах: 2.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Психология

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: "«Психология» курсын оқу барысында сіз алатын білімдер, атап айтсақ, тұлғаның психологиялық табиғаты, ұлттық сананың психологиялық негіздері сияқты сұрақтар қоғам мүшесі ретінде аса қажет, осы курста сізге бағдар бере алатын теориялық-практикалық білімдер өмірде сәттілікке жетуге, бәсекеге қабілетті болуға үйретеді. Теориялық зерттеулер, практикалық психологиялық жаттығулар, көптеген психологиялық әдістер «Мен кіммін?» деген негізгі ішкі сұраққа жауап алуға сізді бағыттайды. Өзіңіздің психологиялық портретіңізді жасайсыз, қоршағандармен позитивті қарым-қатынас жасау психотехнологияларын игересіз, қазіргі ақпараттық технологиялардың дамыған заманында өзіңіздің психологиялық денсаулығыңызды нығайта аласыз. Күнделікті өміріңізде өтіп жатқан жағдайларға орай стресстік күйіңізді өзгерте алатын стресс-менджмент, эмоциялық интеллект, әлеуметтік интеллект пен имиджді қалыптастырудың негіздеріне бағдарланған психологиялық семинар-тренигтік бағдарламаларды игересіз, өзіңізге психологиялық көмек беруге машықтанасыз. Ең бастысы сіз өзіңіздің мотивациялық және эмоциялық әлеміңізді тани отырып сіз позитивті жаққа қарай өзгересіз, өзіңізді жеңесіз, сіз сәтті, ұтқыр, динамикалы, интеллектік әлеуеті жоғары, мықты, бәрі сізге қызыға қарайтын тұлғаға айналасыз. Тұлға құрылымындағы Мен-концепциясын игеріп, өзіңізді адекватты бағалау мен құрметтеуге жол тартасыз!"

    +

    Осваиваемые навыки: "Тұлғаның мотивациялық және эмоциялық ерекшеліктерін зерттеу негізінде ұлттық сана туралы теориялық білімдерін қалыптастыру. Құндылықтар жүйесі және өзіндік анықталу негізінде тұлғаның «психологиялық портретін» түсіндіру. Психологиялық денсаулық және әлеуметтену процесін зерттеу үшін психодиагностикалық әдістерді қолдану. Тұлғаралық қарым-қатынас психологиясынталдау арқылы қоғамдық сананы жаңғыртуда психотехнологияларды пайдалану. Тұлғаның мінез-құлық модельдерін талдау арқылы әлеуметтік интеллект және имидж түсініктерін қалыптастыруға бағытталған психологиялық бағдарлама құрастыру."

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Психология

    +

    Учебная нагрузка в кредитах: 2.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Психология

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: Курс направлен на развитие способностей анализа жизненных и профессиональных ситуаций с помощью социально-психологических знаний. Курс предназначен для повышения общей психологической компетентности и культуры будущего специалиста, осознания своего прошлого, настоящего и будущего с психологических позиций, а также для освоения знаний социально-психологических закономерностей поведения личности.

    +

    Осваиваемые навыки: "Понимать роль психологической науки в системе наук; объяснять роль психологического знания для саморазвития личности и применения в профессиональной деятельности. Понимать сущность и причины психологических явлений и событий, особенности развития и формирования человека как индивида, личности и индивидуальности. Проводить психологическую оценку собственной личности, ситуации, группы людей. Применять психологические знания в решении сложных коммуникативных задач, выхода из конфликтных ситуаций и развитии самоэффективности через решение разных коммуникативных кейсов. Применять навыки саморегуляции личности для профилактики и коррекции негативных психологических состояний."

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Психология

    +

    Учебная нагрузка в кредитах: 2.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Әлеуметтану

    +

    Тип курса: MOOC

    +

    Уровень сложности: средний

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 0.0

    +

    Учебная нагрузка (кредиты): 0.0

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: Not Specified

    +

    Описание курса: Әлеуметтану пәні қоғамның әлеуметтік жағдайын теориялық тұрғыдан түсінуге, сипаттауға және жүйелеуге, тәжірибелік тұрғыдан деректер жинауға мүмкіндік береді. Қоғам күрделі әлеуметтік байланыстар мен қарым-қатынастар жүйесінен тұрады. Мұндай байланыстар өзара серіктестікке, ынтымақтастыққа негізделіп, жасампаздыққа немесе қарама-қайшылықтар мен дау-жанжалдық ситуациялардың әсерімен қиындықтарға да алып келуі мүмкін. Әлеуметтанудың мақсаты - түрлі әлеуметтік байланыстардың қалыптасу заңдылықтарын түсіндіріп, дауларды шешудің тиімді жолдарын көрсету. Әлеуметтік байланыстар мен қарым-қатынастардың қалыптасу сипаты адамдардың құндылықтарымен, өмірге деген көзқарастарымен, мәдениетімен, психологиясымен анықталады және олардың қалай қалыптасатындығы әлеуметтік жағдайға белгілі бір деңгейде байланысты. Сондықтан да әлеуметтануды оқу арқылы өзімізді, өзіміз өмір сүріп отырған ортаны, түрлі топтарды, олардың әлеуметтік психологиясын, мәдениетін, түрлі ситуацияда қандай әрекетке дайын болатындығын түсіне аламыз.

    +

    Осваиваемые навыки: "Әлеуметтану ғылымының зерттеу ерекшеліктерін айқындау негізінде зерттеулерді ұйымдастыруды және алғашқы мәліметтерді жинау әдістерін еркін түрде қолдана білуді қалыптастыру; Қоғамның әлеуметтік құрылымына қатысты нәтижелерді жинақтау арқылы тұлға мен отбасының алатын орны мен әлеуметтену ерекшеліктерін негіздеу; Девиацияның қалыптасу жағдайларына талдау жасау нәтижесінде оларды реттестірудегі, қоғамдық сананы дамытудағы әлеуметтік институттардың маңызын көрсету; Жаһандану жағдайындағы теңсіздіктерді айқындау және ақпараттық қоғамның олардың қалыптасуына әсеріне әлеуметтанулық тұрғыдан қорытынды жасау; Қоғамдағы модернизациялық өзгерістерге, урбанизация процесіне талдау жасау негізінде олардың халықтың әлеуметтік денсаулығының сапалық көрсеткіштеріне әсерін бағалау."

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Әлеуметтану

    +

    Учебная нагрузка в кредитах: 2.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Основные процессы и аппараты химической технологии

    +

    Тип курса: MOOC

    +

    Уровень сложности: продвинутый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 72.288

    +

    Учебная нагрузка (кредиты): 2.41

    +

    Область образования: 6B07 Инженерные, обрабатывающие и строительные отрасли

    +

    Область профессиональной подготовки: 6B071 Инженерия и инженерное дело

    +

    Код и название образовательной программы: B060 Химическая инженерия и процессы

    +

    Описание курса: Курс «Основные процессы и аппараты химической технологии» посвящен общим подходам и приемам по расчету основных параметров процессов химической технологии. Данный курс направлен на то, чтобы студенты и слушатели химико-технологических специальностей могли самостоятельно освоить, закрепить и в дальнейшем проводить анализ, математическое описание и инженерные расчеты для конкретных технологических процессов в своей профессиональной деятельности. Простой и быстрый доступ к учебным материалам по инженерной дисциплине, охватывающей темы по гидродинамическим, тепловым и массообменным процессам, а также свободный график обучения делает данный курс уникальным и удобным для студентов и инженеров химико-технологического профиля без отрыва от производства.

    +

    Осваиваемые навыки: "1. Умение объяснять технологические процессы и устройства химической технологии, используя основные законы сохранения массы, энергии, термодинамического равновесия, кинетики, теории подобия, тепло- и массопереноса. 2. Умение применять расчётные уравнения основных процессов и устройств химической технологии. 3. Умение составлять принципиальные схемы производств химической технологии на основе теплового и материального балансов."

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: 1. Тепловые и массобменные процессы 2.Thermal and mass transfer processes

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. аль-Фараби

    +
    + +
    +

    Физическая химия

    +

    Тип курса: MOOC

    +

    Уровень сложности: продвинутый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 48.192

    +

    Учебная нагрузка (кредиты): 1.606

    +

    Область образования: 6B05 - Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B053 - Физические и химические науки

    +

    Код и название образовательной программы: 6В05301, Химия

    +

    Описание курса: Физикалық химия курында қарастыралтын сұрақтар қазіргі кездегі химия және химиялық тенологияның теориялық фундаменті болады және алған теоориялық білім термодинамика заңдарына сүйене отырып, химиялық процестердің энергетикасын, өздігінен өту бағытын анықтау, гомогенді және гетерогенді жүйелердегі химиялық тепе-теңдік күйге әр түрлі факторлар әсерін және ерітінділер заңдылықтарын, кинетика және электрохимия негіздерін білу қажет.

    +

    Осваиваемые навыки: Ұсынылып отрыған бағдарлама жоғары сынып оқушылары мен химия мамандықтары студенттерінің базалық білімін нығайтып, химиялық процестердің энергетикалық сипаттамаларын есептеудің теориясы мен практикасы бойынша жалпы және пәндік құзыреттілікті қалыптастыру.

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Физикалық химия

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им.аль-Фараби

    +
    + +
    +

    Органическая химия алифатических соединений

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 18.072

    +

    Учебная нагрузка (кредиты): 0.602

    +

    Область образования: 6B07 Инженерные, обрабатывающие и строительные отрасли

    +

    Область профессиональной подготовки: 6B072 Производственные и обрабатывающие отрасли

    +

    Код и название образовательной программы: 6B07104 Химическая технология органических веществ

    +

    Описание курса: Сформировать способность применять основы органической химии для описания и оценивания свойств, методы синтеза алифатических и циклических органических соединений, выявлять достоинства, недостатки, предлагать пути модификации. Курс формирует теоретико-методологическую основу понимания основных закономерностей: строение и реакционная способность алифатических и циклических органических соединений. Будут рассмотрены: классификация, строения, изомерия, способы получения, свойства и применения.

    +

    Осваиваемые навыки: Сформировать способность применять основы органической химии для описания и оценивания свойств, методы синтеза алифатических и циклических органических соединений, выявлять достоинства, недостатки, предлагать пути модификации. Курс формирует теоретико-методологическую основу понимания основных закономерностей: строение и реакционная способность алифатических и циклических органических соединений. Будут рассмотрены: классификация, строения, изомерия, способы получения, свойства и применения.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: "Органическая химия алифатических и циклических соединений "

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им.аль-Фараби

    +
    + +
    +

    Органическая химия алифатических соединений

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 18.072

    +

    Учебная нагрузка (кредиты): 0.602

    +

    Область образования: 6B07 Инженерные, обрабатывающие и строительные отрасли

    +

    Область профессиональной подготовки: 6B072 Производственные и обрабатывающие отрасли

    +

    Код и название образовательной программы: 6B07102 Химическая инженерия

    +

    Описание курса: Сформировать способность устанавливать связь между строением органических веществ и их реакционной способностью, классифицировать методы синтеза основных классов органических соединений и качественные химические реакции на функциональные группы, методы выделения и идентификации органических соединений, использовать навыки работы с веществами, лабораторными установками в практической деятельности.

    +

    Осваиваемые навыки: Сформировать способность устанавливать связь между строением органических веществ и их реакционной способностью, классифицировать методы синтеза основных классов органических соединений и качественные химические реакции на функциональные группы, методы выделения и идентификации органических соединений, использовать навыки работы с веществами, лабораторными установками в практической деятельности.

    +

    Язык курса: русский

    +

    Дисциплины для перезачета: Химия углеводородов и их функциональных производных

    +

    Учебная нагрузка в кредитах: 9.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им.аль-Фараби

    +
    + +
    +

    Биохимия

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Coursera

    +

    Учебная нагрузка (часы): 12.048

    +

    Учебная нагрузка (кредиты): 0.402

    +

    Область образования: 6B05 - Естественные науки, математика и статистика

    +

    Область профессиональной подготовки: 6B053 - Физические и химические науки

    +

    Код и название образовательной программы: 6В05301, Химия

    +

    Описание курса: Курс Энергия алмасудың негізгі сатыларын ажырату, оның биохимиялық принциптерін түсінуге мүмкіндік береді

    +

    Осваиваемые навыки: Студенттер Энергия алмасуының сатыларын ажырататын болады.

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Биохимия

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: Korea Advanced Institute of Science and Technology

    +
    + +
    +

    Медика-биологиялық мақсаттағы полимерлер химиясы

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 15.662

    +

    Учебная нагрузка (кредиты): 0.522

    +

    Область образования: 6B07 Инженерные, обрабатывающие и строительные отрасли

    +

    Область профессиональной подготовки: 6B072 Производственные и обрабатывающие отрасли

    +

    Код и название образовательной программы: 6В07201-Фармацептикалық технология өндірісі

    +

    Описание курса: Курс медика-биологиялық мақсаттағы полимерлердің сипаттамаларын, фармацевтика мен медицинада қолдану мақсаттарына сәйкес оларға қойылатын негізгі талаптарды, оларды синтездеу және модификациялау әдістерін, сонымен қатар негізгі физикалық және химиялық қасиеттері мен олардың практикалық қолдану аспектілері жан-жақты оқытуға арналған.

    +

    Осваиваемые навыки: "ОН 1 Биомедициналық полимерлердің түсініктері мен терминдерін, жіктелуі мен номенклатурасын; медика-биологиялық мақсаттағы полимерлі материалдарға қойылатын талаптарды сипаттауға ОН 2 Медициналық дәрежеде таза және бағытталған биологиялық әсер ететін полимерлерді синтездеу әдістерін сипаттауға ОН 3 Биомедициналық мақсаттағы полимерлер мен полимерлік материалдардың синтезін жүзеге асыра алу дағдыларын көрсету ОН 4 Еріткіштің термодинамикалық сапасын, макромолекулалардың молекулалық-массалық және басқа да сипаттамаларын бағалау үшін полимерлердің ерітінділерін зерттеу әдістерін қолдану "

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Медика-биологиялық мақсаттағы полимерлер химиясы

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. Аль-Фараби

    +
    + +
    +

    Аналитикалық химия

    +

    Тип курса: MOOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 15.662

    +

    Учебная нагрузка (кредиты): 0.522

    +

    Область образования: Not Specified

    +

    Область профессиональной подготовки: Not Specified

    +

    Код и название образовательной программы: 6B05 - Естественные науки, математика и статистика

    +

    Описание курса: Базалық пән бойынша терең теориялық білім қалыптастыру.Химиялық талдау бойынша толық мәлімет беру арқылы базалық білімдерін қалыптастыру.

    +

    Осваиваемые навыки: "Негізгі пән ретінде толықтырылған әрі терең білім беру. ЖАОК-ның қолдауы: ЖАОК дәстүрлі оқытуда СӨЖ ретінде, «Төңкерілген сынып» технологиясын қолдана отырып немесе қолданбай-ақ пән бойынша қосымша материал ретінде қолданылады. "

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Химиялық талдау 1

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. Аль-Фараби

    +
    + +
    +

    Аналитикалық химия. Сапалық талдау.

    +

    Тип курса: SPOC

    +

    Уровень сложности: базовый

    +

    Платформа: Openkaznu

    +

    Учебная нагрузка (часы): 12.048

    +

    Учебная нагрузка (кредиты): 0.402

    +

    Область образования: 6B07 Инженерные, обрабатывающие и строительные отрасли

    +

    Область профессиональной подготовки: 6B072 Производственные и обрабатывающие отрасли

    +

    Код и название образовательной программы: "В068 Производство продуктов питания "

    +

    Описание курса: Базалық пән бойынша терең теориялық және практикалық білім қалыптастыру. Сапалық талдау бойынша толық мәлімет беру арқылы базалық білімдерін қалыптастыру.

    +

    Осваиваемые навыки: "Негізгі пән ретінде толықтырылған әрі терең білім беру. SPOC-ның қолдауы: SPOC дәстүрлі оқытуда СӨЖ ретінде пән бойынша қосымша материал ретінде қолданылады. "

    +

    Язык курса: казахский

    +

    Дисциплины для перезачета: Азық-түліктің талдауы

    +

    Учебная нагрузка в кредитах: 5.0

    +

    Ссылка на курс: Открыть курс

    +

    Организация: КазНУ им. Аль-Фараби + + +чтобы все фильтры работали как у других

    +
    +
    + +
    + + + + + diff --git a/lms/templates/static_templates/competition.html b/lms/templates/static_templates/competition.html new file mode 100644 index 000000000000..892e262fd3a3 --- /dev/null +++ b/lms/templates/static_templates/competition.html @@ -0,0 +1,644 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Competition")} + + + + + + Конкурс педагогических сценариев онлайн-курсов + + + + + + +
    +
    +

    ${_("Competition for the Selection of Pedagogical Scenarios for Online Courses")}

    +

    ${_("Al-Farabi Kazakh National University")}

    +
    +
    + +
    + +
    +
    +

    ${_("About the Competition")}

    +
    + +
    +

    ${_("The Office of Academic and Digital Innovations is accepting applications for participation in the competition of pedagogical scenarios for the creation and publication of online courses (MOOC/SPOC)")}.

    +
    +

    ${_("Attention!")}

    +

    ${_("The competition for pedagogical scenarios for the development and publication of online courses (MOOC/SPOC), organized by the Office of Academic and Digital Innovations, has been canceled in 2026.")}

    +

    ${_("Order on cancellation.")}

    +

    ${_("Information about the next competition will be published on this page.")}

    + +
    +
    +

    ${_("MOOC Competition Regulations")}

    + +
    + +

    ${_("Both creative teams of university departments and individual teaching or research staff of the university can participate in the competition")}.

    +

    ${_("Based on the results of the competition, the commission will approve and release 5 MOOC and 5 SPOC")}.

    +
    +
    + + +
    +
    +

    ${_("Competition Stages")}

    +
    + +
    +
    +
    + ${_("Stage")} 1 +

    ${_("Registration and Submission of Applications")}

    +

    ${_("Until")} ${_("February")} 10, 2025

    +
    +
    + +
    +
    + ${_("Stage")} 2 +

    ${_("Public Defense of Pedagogical Scenarios")}

    +

    ${_("Until")} ${_("February")} 17, 2025

    +
    +
    + +
    +
    + ${_("Stage")} 3 +

    ${_("Evaluation of Applications and Selection of Winners")}

    +

    ${_("Until")} ${_("February")} 24, 2025

    +
    +
    + +
    +
    + ${_("Stage")} 4 +

    ${_("Approval of Competition Results")}

    +

    ${_("Until")} ${_("March")} 3, 2025

    +
    +
    +
    +
    + + +
    +
    +

    ${_("Application Requirements")}

    +
    + +
    +
    +
    +
    + +
    +

    ${_("Cover Letter")}

    + +
    + +
    +
    + +
    +

    ${_("Pedagogical Scenario of an Online Course")}

    + +
    + +
    +
    + +
    +

    ${_("Lecture Video Recording")}

    + +
    +
    + +
    +
    +
    + +
    +

    ${_("Online Course Certificate")}

    + +
    + +
    +
    + +
    +

    ${_("Author Information")}

    + +
    +
    +
    +
    + + +
    +
    +

    ${_("Competition Announcement")}

    +
      +
    • + ${_("The Office of Academic and Digital Innovations")} ${_("is pleased to announce the launch of the competition for the selection of pedagogical scenarios for online courses.")} +
    • +
    • + ${_("Stage")} 1: ${_("Application submission")} — ${_("February")} 10–17, 2025 +
    • +
    • + ${_("Stage")} 2: ${_("Selection of applications for participation")} — ${_("February")} 18–24, 2025 +
    • +
    • + ${_("Stage")} 3: ${_("Public defense of projects")} — ${_("February")} 25–28, 2025 +
    • +
    • + ${_("Stage")} 4: ${_("Announcement of winners")} — ${_("March")} 3–10, 2025 +
    • +
    • + ${_("Applications are accepted online via the form on the Open Kaznu website. Details and application templates can be found on the competition page")}. +
    • +
    +
    +
    + + + + + +
    +

    ${_("Contacts")}

    +

    ${_("For all inquiries, please contact the Office of Academic and Digital Innovations: Building - Rectorate, Office 207, phone")} +7 727 377 33 30 ${_("(ext.")} 31-24, 11-33, 16-47)

    +
    +
    + + + diff --git a/lms/templates/static_templates/detect.html b/lms/templates/static_templates/detect.html new file mode 100644 index 000000000000..2bbe171a40d2 --- /dev/null +++ b/lms/templates/static_templates/detect.html @@ -0,0 +1,244 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Detect")} + + + + + + + + + + + +
    +
    +

    ${_("Chatbot Usage Verification System")}

    +

    ${_("Al-Farabi Kazakh National University")}

    +
    +
    + +
    + +
    +
    +

    ${_("Content Verification")}

    +
    + +
    +

    ${_("Content Analysis Tool")}

    +

    ${_("Use the built-in verification system to analyze text content and determine the likelihood of it being created with chatbots or artificial intelligence.")}

    + +
    + +
    +
    ${_("Open in full size")}
    +
    +
    + +

    ${_("This tool allows you to check texts for AI-generated content, which is especially important for ensuring academic integrity in the educational process.")}

    +
    +
    + + +
    +

    ${_("Contacts")}

    +

    ${_("For all inquiries, please contact the Office of Academic and Digital Innovations: Building - Rectorate, Office 207, phone")} +7 727 377 33 30 ${_("(ext.")} 31-24, 11-33, 16-47)

    +
    +
    + + + + + diff --git a/lms/templates/static_templates/honor_code.html b/lms/templates/static_templates/honor_code.html new file mode 100644 index 000000000000..22f7a80df7c4 --- /dev/null +++ b/lms/templates/static_templates/honor_code.html @@ -0,0 +1,16 @@ +<%page expression_filter="h"/> +<%! from django.utils.translation import gettext as _ %> +<%inherit file="../main.html" /> + +<%block name="pagetitle">${_("Donate")} + +
    +
    +

    + <%block name="pageheader">${page_header or _("Donate")} +

    +

    + <%block name="pagecontent">${page_content or _("This page left intentionally blank. Feel free to add your own content.")} +

    +
    +
    diff --git a/lms/templates/word_cloud.html b/lms/templates/word_cloud.html index 9e2623f19379..a183944ecf56 100644 --- a/lms/templates/word_cloud.html +++ b/lms/templates/word_cloud.html @@ -7,7 +7,7 @@ data-ajax-url="${ajax_url}" > % if display_name: -

    ${display_name}

    +

    ${display_name}

    % endif % if instructions is not None: diff --git a/lms/urls.py b/lms/urls.py index 1ae425c9a9be..b44993ecb61c 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -16,6 +16,7 @@ from common.djangoapps.student import views as student_views from common.djangoapps.util import views as util_views from lms.djangoapps.branding import views as branding_views +from lms.djangoapps.course_admin import views as course_admin_views from lms.djangoapps.courseware.masquerade import MasqueradeView from lms.djangoapps.courseware.block_render import ( handle_xblock_callback, @@ -50,7 +51,8 @@ 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.univerapi import views as univer_views +from lms.djangoapps.news import views as news_views RESET_COURSE_DEADLINES_NAME = 'reset_course_deadlines' RENDER_XBLOCK_NAME = 'render_xblock' RENDER_VIDEO_XBLOCK_NAME = 'render_public_video_xblock' @@ -94,6 +96,15 @@ urlpatterns = [ + re_path(r'^course-admin/?$', course_admin_views.course_admin, name='course_admin'), + path('news/', news_views.news_list, name='news_list'), + path('news//', news_views.news_detail, name='news_detail'), + path('news/create/', news_views.news_create, name='news_create'), + path('analyze/', news_views.analyze, name='analyze'), + path('analyze/course_details//', news_views.course_details, name='course_details'), + path('go-to-exam/', news_views.go_to_exam, name='exam'), + path('finish-exam/', news_views.finish_exam, name='finish_exam'), + path('api/univertest/', univer_views.UniverTestView.as_view(), name='univer_test'), path('', branding_views.index, name='root'), # Main marketing page, or redirect to courseware path('', include('common.djangoapps.student.urls')), diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0030_courseoverview_complexity_and_more.py b/openedx/core/djangoapps/content/course_overviews/migrations/0030_courseoverview_complexity_and_more.py new file mode 100644 index 000000000000..75f46e3ce6b5 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0030_courseoverview_complexity_and_more.py @@ -0,0 +1,40 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "course_overviews", + "0029_alter_historicalcourseoverview_options", + ), + ] + + operations = [ + migrations.AddField( + model_name="courseoverview", + name="complexity", + field=models.CharField( + max_length=10, + default="medium", + choices=[ + ("easy", "Easy"), + ("medium", "Medium"), + ("hard", "Hard"), + ], + ), + ), + migrations.AddField( + model_name="historicalcourseoverview", + name="complexity", + field=models.CharField( + max_length=10, + default="medium", + choices=[ + ("easy", "Easy"), + ("medium", "Medium"), + ("hard", "Hard"), + ], + ), + ), + ] diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0031_courseoverview_faculty_directions.py b/openedx/core/djangoapps/content/course_overviews/migrations/0031_courseoverview_faculty_directions.py new file mode 100644 index 000000000000..702f8574a024 --- /dev/null +++ b/openedx/core/djangoapps/content/course_overviews/migrations/0031_courseoverview_faculty_directions.py @@ -0,0 +1,34 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "course_overviews", + "0030_courseoverview_complexity_and_more", + ), + ] + + operations = [ + migrations.AddField( + model_name="courseoverview", + name="faculty", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="courseoverview", + name="directions", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="historicalcourseoverview", + name="faculty", + field=models.TextField(null=True), + ), + migrations.AddField( + model_name="historicalcourseoverview", + name="directions", + field=models.TextField(null=True), + ), + ] diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py index 10a56f0868fb..07f471e9d5bf 100644 --- a/openedx/core/djangoapps/content/course_overviews/models.py +++ b/openedx/core/djangoapps/content/course_overviews/models.py @@ -59,6 +59,11 @@ class CourseOverview(TimeStampedModel): .. no_pii: """ + COMPLEXITY_CHOICES = [ + ("easy", "Easy"), + ("medium", "Medium"), + ("hard", "Hard"), + ] class Meta: app_label = 'course_overviews' @@ -126,6 +131,17 @@ class Meta: short_description = models.TextField(null=True) course_video_url = models.TextField(null=True) effort = models.TextField(null=True) + # New column + complexity = models.CharField( + max_length=10, + choices=COMPLEXITY_CHOICES, + default="medium", + null=False, + blank=False, + ) + faculty = models.TextField(null=True) + directions = models.TextField(null=True) + self_paced = models.BooleanField(default=False) marketing_url = models.TextField(null=True) eligible_for_financial_aid = models.BooleanField(default=True) diff --git a/openedx/core/djangoapps/models/course_details.py b/openedx/core/djangoapps/models/course_details.py index c00d7d0b8816..dd60852cc902 100644 --- a/openedx/core/djangoapps/models/course_details.py +++ b/openedx/core/djangoapps/models/course_details.py @@ -27,6 +27,7 @@ 'short_description', 'overview', 'effort', + # 'complexity', 'entrance_exam_enabled', 'entrance_exam_id', 'entrance_exam_minimum_score_pct', @@ -193,6 +194,7 @@ def update_about_video(cls, course, video_id, user_id): @classmethod def update_from_json(cls, course_key, jsondict, user): # pylint: disable=too-many-statements + # print(">>> PAYLOAD:", jsondict) """ Decode the json into CourseDetails and save any changed attrs to the db """ @@ -309,7 +311,9 @@ def update_from_json(cls, course_key, jsondict, user): # pylint: disable=too-ma # the fields actually changed to make faster, could compare # against db or could have client send over a list of which # fields changed. + # print(">>> ATTRIBUTES:", ABOUT_ATTRIBUTES) for attribute in ABOUT_ATTRIBUTES: + if attribute in jsondict: cls.update_about_item(block, attribute, jsondict[attribute], user.id) diff --git a/openedx/core/djangoapps/user_api/accounts/__init__.py b/openedx/core/djangoapps/user_api/accounts/__init__.py index caf78ca54ae0..56a8614be638 100644 --- a/openedx/core/djangoapps/user_api/accounts/__init__.py +++ b/openedx/core/djangoapps/user_api/accounts/__init__.py @@ -93,7 +93,7 @@ # Translators: These messages are shown to users who do not enter information # into the required field or enter it incorrectly. -REQUIRED_FIELD_NAME_MSG = _("Enter your full name") +REQUIRED_FIELD_NAME_MSG = _("Enter your surname and name") REQUIRED_FIELD_FIRST_NAME_MSG = _("Enter your first name") REQUIRED_FIELD_LAST_NAME_MSG = _("Enter your last name") REQUIRED_FIELD_CONFIRM_EMAIL_MSG = _("The email addresses do not match") @@ -109,7 +109,7 @@ REQUIRED_FIELD_LEVEL_OF_EDUCATION_MSG = _("Select the highest level of education you have completed") REQUIRED_FIELD_YEAR_OF_BIRTH_MSG = _("Select your year of birth") REQUIRED_FIELD_GENDER_MSG = _("Select your gender") -REQUIRED_FIELD_MAILING_ADDRESS_MSG = _("Enter your mailing address") +REQUIRED_FIELD_MAILING_ADDRESS_MSG = _("Enter your educational institution") # HIBP Strings AUTHN_LOGIN_BLOCK_HIBP_POLICY_MSG = _( diff --git a/openedx/core/djangoapps/user_api/views.py b/openedx/core/djangoapps/user_api/views.py index d52493556a19..cef77e8708e3 100644 --- a/openedx/core/djangoapps/user_api/views.py +++ b/openedx/core/djangoapps/user_api/views.py @@ -24,6 +24,9 @@ ) from openedx.core.lib.api.permissions import ApiKeyHeaderPermission from openedx.core.lib.api.view_utils import require_post_params +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters + class UserViewSet(viewsets.ReadOnlyModelViewSet): diff --git a/openedx/core/djangoapps/user_authn/api/form_fields.py b/openedx/core/djangoapps/user_authn/api/form_fields.py index ca001ba3f1b5..f21e494c113b 100644 --- a/openedx/core/djangoapps/user_authn/api/form_fields.py +++ b/openedx/core/djangoapps/user_authn/api/form_fields.py @@ -255,7 +255,7 @@ def add_mailing_address_field(is_field_required=False): """ # Translators: This label appears above a field # meant to hold the user's mailing address. - mailing_address_label = _("Mailing address") + mailing_address_label = _("Educational institution") return { 'name': 'mailing_address', diff --git a/openedx/core/djangoapps/user_authn/views/registration_form.py b/openedx/core/djangoapps/user_authn/views/registration_form.py index efee92e700b7..f92ddb5a6f8f 100644 --- a/openedx/core/djangoapps/user_authn/views/registration_form.py +++ b/openedx/core/djangoapps/user_authn/views/registration_form.py @@ -197,7 +197,7 @@ def __init__( "level_of_education": _("A level of education is required"), "gender": _("Your gender is required"), "year_of_birth": _("Your year of birth is required"), - "mailing_address": _("Your mailing address is required"), + "mailing_address": _("Your educational institution is required"), "goals": _("A description of your goals is required"), "city": _("A city is required"), "country": _("A country is required") @@ -557,7 +557,7 @@ def _add_name_field(self, form_desc, required=True): # Translators: These instructions appear on the registration form, immediately # below a field meant to hold the user's full name. - name_instructions = _("This name will be used on any certificates that you earn.") + name_instructions = _("Surname and Name") form_desc.add_field( "name", @@ -795,7 +795,7 @@ def _add_mailing_address_field(self, form_desc, required=True): """ # Translators: This label appears above a field on the registration form # meant to hold the user's mailing address. - mailing_address_label = _("Mailing address") + mailing_address_label = _("Educational institution") error_msg = accounts.REQUIRED_FIELD_MAILING_ADDRESS_MSG form_desc.add_field( diff --git a/setup.cfg b/setup.cfg index 122a61370102..042ff17bab1f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -165,6 +165,8 @@ ignore_imports = # -> openedx.features.enterprise_support.utils openedx.features.enterprise_support.utils -> lms.djangoapps.branding.api cms.djangoapps.contentstore.rest_api.v1.views.settings -> lms.djangoapps.certificates.api + cms.djangoapps.contentstore.course_admin -> lms.djangoapps.certificates.api + cms.djangoapps.contentstore.views.course_admin -> lms.djangoapps.certificates.api [importlinter:contract:2] diff --git a/xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css b/xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css index 85ca354eb99f..bc1b79733fdf 100644 --- a/xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css +++ b/xmodule/static/css-builtin-blocks/WordCloudBlockDisplay.css @@ -25,3 +25,6 @@ font-size: 0.85em; display: block; } +.xmodule_display.xmodule_WordCloudBlock .hd.hd-3 { + font-weight: bold !important; +}