Files
edx-platform/cms/urls.py
Kyle McCormick 20bc7113e3 feat!: Remove Studio Maintenance & Announcements (#37432)
The announcements editor was never ported to frontend-app-authoring, and
the announcements display was never ported to frontend-app-learner-dashboard.
This announcements feature is rarely used, undocumented, non-a11y-friendly, and
there were no volunteers to port it to the new frontends. It is the last
remaining part of the legacy Studio "Maintenance" dashboard. So, we are
removing it.

BREAKING CHANGE: This removes...
* Studio Maintenance dashboard legacy frontend
* Studio Edit Announcements legacy frontend
* The snippet of legacy learner dashboard which renders announcements
* openedx/features/announcements djangoapp
* The ENABLE_ANNOUNCEMENTS feature flag

Not removed:
* The announcements_announcement table from openedx/features/announcements .
  The table is most likely very small, as it is only populated by administrators. Removing
  it would be more labor for us and more risk of toil for operators than is worthwhile.

Closes: https://github.com/openedx/edx-platform/issues/36263
2025-10-10 12:48:00 -04:00

371 lines
18 KiB
Python

"""
Urls of Studio.
"""
from django.conf import settings
from django.conf.urls.static import static
from django.contrib.admin import autodiscover as django_autodiscover
from django.urls import include
from django.urls import path, re_path
from django.utils.translation import gettext_lazy as _
from django.contrib import admin
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
from auth_backends.urls import oauth2_urlpatterns
from edx_api_doc_tools import make_docs_urls
import openedx.core.djangoapps.common_views.xblock
import openedx.core.djangoapps.debug.views
import openedx.core.djangoapps.lang_pref.views
from cms.djangoapps.contentstore import toggles
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 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
from openedx.core import toggles as core_toggles
django_autodiscover()
admin.site.site_header = _('Studio Administration')
admin.site.site_title = admin.site.site_header
if password_policy_compliance.should_enforce_compliance_on_login():
admin.site.login_form = PasswordPolicyAwareAdminAuthForm
# Custom error pages
# These are used by Django to render these error codes. Do not remove.
# pylint: disable=invalid-name
handler404 = contentstore_views.render_404
handler500 = contentstore_views.render_500
# Pattern to match a course key or a library key
COURSELIKE_KEY_PATTERN = r'(?P<course_key_string>({}|{}))'.format(
r'[^/]+/[^/]+/[^/]+', r'[^/:]+:[^/+]+\+[^/+]+(\+[^/]+)?'
)
# Pattern to match a library key only
LIBRARY_KEY_PATTERN = r'(?P<library_key_string>library-v1:[^/+]+\+[^/+]+)'
# oauth2_urlpatterns needs to be first to override any other login and
# logout related views.
urlpatterns = oauth2_urlpatterns + [
path('', include('openedx.core.djangoapps.user_authn.urls_common')),
path('', include('common.djangoapps.student.urls')),
path('transcripts/upload', contentstore_views.upload_transcripts, name='upload_transcripts'),
path('transcripts/download', contentstore_views.download_transcripts, name='download_transcripts'),
path('transcripts/check', contentstore_views.check_transcripts, name='check_transcripts'),
path('transcripts/choose', contentstore_views.choose_transcripts, name='choose_transcripts'),
path('transcripts/replace', contentstore_views.replace_transcripts, name='replace_transcripts'),
path('transcripts/rename', contentstore_views.rename_transcripts, name='rename_transcripts'),
re_path(r'^preview/xblock/(?P<usage_key_string>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>.*))?$',
contentstore_views.preview_handler, name='preview_handler'),
re_path(r'^xblock/(?P<usage_key_string>.*?)/handler/(?P<handler>[^/]*)(?:/(?P<suffix>.*))?$',
contentstore_views.component_handler, name='component_handler'),
re_path(r'^xblock/resource/(?P<block_type>[^/]*)/(?P<uri>.*)$',
openedx.core.djangoapps.common_views.xblock.xblock_resource, name='xblock_resource_url'),
path('', include('openedx.core.djangoapps.xblock.rest_api.urls', namespace='xblock_api')),
path('not_found', contentstore_views.not_found, name='not_found'),
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')),
# noop to squelch ajax errors
path('event', contentstore_views.event, name='event'),
path('heartbeat', include('openedx.core.djangoapps.heartbeat.urls')),
path('i18n/', include('django.conf.urls.i18n')),
# Course assets
path('', include('openedx.core.djangoapps.contentserver.urls')),
# User API endpoints
path('api/user/', include('openedx.core.djangoapps.user_api.urls')),
# Update session view
path('lang_pref/update_language', openedx.core.djangoapps.lang_pref.views.update_language,
name='update_language'
),
# Darklang View to change the preview language (or dark language)
path('update_lang/', include('openedx.core.djangoapps.dark_lang.urls', namespace='dark_lang')),
# For redirecting to help pages.
path('help_token/', include('help_tokens.urls')),
path('api/', include('cms.djangoapps.api.urls', namespace='api')),
# restful api
path('', contentstore_views.howitworks, name='homepage'),
path('howitworks', contentstore_views.howitworks, name='howitworks'),
path('signin_redirect_to_lms', contentstore_views.login_redirect_to_lms, name='login_redirect_to_lms'),
path('request_course_creator', contentstore_views.request_course_creator, name='request_course_creator'),
re_path(fr'^course_team/{COURSELIKE_KEY_PATTERN}(?:/(?P<email>.+))?$',
contentstore_views.course_team_handler, name='course_team_handler'),
re_path(fr'^course_info/{settings.COURSE_KEY_PATTERN}$', contentstore_views.course_info_handler,
name='course_info_handler'),
re_path(fr'^course_info_update/{settings.COURSE_KEY_PATTERN}/(?P<provided_id>\d+)?$',
contentstore_views.course_info_update_handler, name='course_info_update_handler'
),
re_path(r'^home/?$', contentstore_views.course_listing, name='home'),
re_path(r'^home_library/?$', contentstore_views.library_listing, name='home_library'),
re_path(fr'^course/{settings.COURSE_KEY_PATTERN}/search_reindex?$',
contentstore_views.course_search_index_handler,
name='course_search_index_handler'
),
re_path(fr'^course/{settings.COURSE_KEY_PATTERN}?$', contentstore_views.course_handler, name='course_handler'),
re_path(fr'^checklists/{settings.COURSE_KEY_PATTERN}?$',
contentstore_views.checklists_handler,
name='checklists_handler'),
re_path(fr'^course_notifications/{settings.COURSE_KEY_PATTERN}/(?P<action_state_id>\d+)?$',
contentstore_views.course_notifications_handler,
name='course_notifications_handler'),
re_path(fr'^course_rerun/{settings.COURSE_KEY_PATTERN}$', contentstore_views.course_rerun_handler,
name='course_rerun_handler'),
re_path(fr'^container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.container_handler,
name='container_handler'),
re_path(fr'^container_embed/{settings.USAGE_KEY_PATTERN}$', contentstore_views.container_embed_handler,
name='container_embed_handler'),
re_path(fr'^orphan/{settings.COURSE_KEY_PATTERN}$', contentstore_views.orphan_handler,
name='orphan_handler'),
re_path(fr'^assets/{settings.COURSE_KEY_PATTERN}/{settings.ASSET_KEY_PATTERN}?$',
contentstore_views.assets_handler,
name='assets_handler'),
re_path(fr'^assets/{settings.COURSE_KEY_PATTERN}/{settings.ASSET_KEY_PATTERN}/usage',
contentstore_views.asset_usage_path_handler,
name='asset_usage_path_handler'),
re_path(fr'^import/{COURSELIKE_KEY_PATTERN}$', contentstore_views.import_handler,
name='import_handler'),
re_path(fr'^import_status/{COURSELIKE_KEY_PATTERN}/(?P<filename>.+)$',
contentstore_views.import_status_handler, name='import_status_handler'),
# rest api for course import/export
path('api/courses/', include('cms.djangoapps.contentstore.api.urls', namespace='courses_api')
),
path('api/modulestore_migrator/',
include('cms.djangoapps.modulestore_migrator.rest_api.urls', namespace='modulestore_migrator_api')),
re_path(fr'^export/{COURSELIKE_KEY_PATTERN}$', contentstore_views.export_handler,
name='export_handler'),
re_path(fr'^export_output/{COURSELIKE_KEY_PATTERN}$', contentstore_views.export_output_handler,
name='export_output_handler'),
re_path(fr'^export_status/{COURSELIKE_KEY_PATTERN}$', contentstore_views.export_status_handler,
name='export_status_handler'),
re_path(fr'^xblock/outline/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_outline_handler,
name='xblock_outline_handler'),
re_path(fr'^xblock/container/{settings.USAGE_KEY_PATTERN}$', contentstore_views.xblock_container_handler,
name='xblock_container_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/action/edit$', xblock_edit_view,
name='xblock_edit_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}/(?P<view_name>[^/]+)$', contentstore_views.xblock_view_handler,
name='xblock_view_handler'),
re_path(fr'^xblock/{settings.USAGE_KEY_PATTERN}?$', contentstore_views.xblock_handler,
name='xblock_handler'),
re_path(fr'^tabs/{settings.COURSE_KEY_PATTERN}$', contentstore_views.tabs_handler,
name='tabs_handler'),
re_path(fr'^settings/details/{settings.COURSE_KEY_PATTERN}$', contentstore_views.settings_handler,
name='settings_handler'),
re_path(fr'^settings/grading/{settings.COURSE_KEY_PATTERN}(/)?(?P<grader_index>\d+)?$',
contentstore_views.grading_handler, name='grading_handler'),
re_path(fr'^settings/advanced/{settings.COURSE_KEY_PATTERN}$', contentstore_views.advanced_settings_handler,
name='advanced_settings_handler'),
re_path(fr'^textbooks/{settings.COURSE_KEY_PATTERN}$', contentstore_views.textbooks_list_handler,
name='textbooks_list_handler'),
re_path(fr'^textbooks/{settings.COURSE_KEY_PATTERN}/(?P<textbook_id>\d[^/]*)$',
contentstore_views.textbooks_detail_handler, name='textbooks_detail_handler'),
re_path(fr'^videos/{settings.COURSE_KEY_PATTERN}(?:/(?P<edx_video_id>[-\w]+))?$',
contentstore_views.videos_handler, name='videos_handler'),
re_path(fr'^generate_video_upload_link/{settings.COURSE_KEY_PATTERN}$',
contentstore_views.generate_video_upload_link_handler, name='generate_video_upload_link'),
re_path(fr'^video_images/{settings.COURSE_KEY_PATTERN}(?:/(?P<edx_video_id>[-\w]+))?$',
contentstore_views.video_images_handler, name='video_images_handler'),
path('video_images_upload_enabled', contentstore_views.video_images_upload_enabled,
name='video_images_upload_enabled'),
path('video_features/', contentstore_views.get_video_features, name='video_features'),
re_path(fr'^transcript_preferences/{settings.COURSE_KEY_PATTERN}$',
contentstore_views.transcript_preferences_handler, name='transcript_preferences_handler'),
re_path(fr'^transcript_credentials/{settings.COURSE_KEY_PATTERN}$',
contentstore_views.transcript_credentials_handler, name='transcript_credentials_handler'),
path('transcript_download/', contentstore_views.transcript_download_handler, name='transcript_download_handler'),
path('transcript_upload/', contentstore_views.transcript_upload_handler, name='transcript_upload_handler'),
re_path(r'^transcript_delete/{}(?:/(?P<edx_video_id>[-\w]+))?(?:/(?P<language_code>[^/]*))?$'.format(
settings.COURSE_KEY_PATTERN
), contentstore_views.transcript_delete_handler, name='transcript_delete_handler'),
path('transcript_upload_api/', contentstore_views.transcript_upload_api, name='transcript_upload_api'),
re_path(fr'^video_encodings_download/{settings.COURSE_KEY_PATTERN}$',
contentstore_views.video_encodings_download, name='video_encodings_download'),
re_path(fr'^group_configurations/{settings.COURSE_KEY_PATTERN}$',
contentstore_views.group_configurations_list_handler,
name='group_configurations_list_handler'),
re_path(r'^group_configurations/{}/(?P<group_configuration_id>\d+)(/)?(?P<group_id>\d+)?$'.format(
settings.COURSE_KEY_PATTERN), contentstore_views.group_configurations_detail_handler,
name='group_configurations_detail_handler'),
path('api/val/v0/', include('edxval.urls')),
path('api/tasks/v0/', include('user_tasks.urls')),
path('accessibility', contentstore_views.accessibility, name='accessibility'),
re_path(fr'api/youtube/courses/{COURSELIKE_KEY_PATTERN}/edx-video-ids$',
contentstore_views.get_course_youtube_edx_videos_ids, name='youtube_edx_video_ids'),
re_path(fr'^api/courses/{settings.COURSE_KEY_PATTERN}/bulk_enable_disable_discussions$',
contentstore_views.bulk_enable_disable_discussions,
name='bulk_enable_disable_discussions'),
]
if not settings.DISABLE_DEPRECATED_SIGNIN_URL:
# TODO: Remove deprecated signin url when traffic proves it is no longer in use
urlpatterns += [
path('signin', contentstore_views.login_redirect_to_lms),
]
if not settings.DISABLE_DEPRECATED_SIGNUP_URL:
# TODO: Remove deprecated signup url when traffic proves it is no longer in use
urlpatterns += [
path('signup', contentstore_views.register_redirect_to_lms, name='register_redirect_to_lms'),
]
JS_INFO_DICT = {
'domain': 'djangojs',
# We need to explicitly include external Django apps that are not in LOCALE_PATHS.
'packages': ('openassessment',),
}
urlpatterns += [
path('openassessment/fileupload/', include('openassessment.fileupload.urls')),
]
if toggles.ENABLE_CONTENT_LIBRARIES:
urlpatterns += [
re_path(fr'^library/{LIBRARY_KEY_PATTERN}?$',
contentstore_views.library_handler, name='library_handler'),
re_path(fr'^library/{LIBRARY_KEY_PATTERN}/team/$',
contentstore_views.manage_library_users, name='manage_library_users'),
]
if toggles.EXPORT_GIT.is_enabled():
urlpatterns += [
re_path(fr'^export_git/{settings.COURSE_KEY_PATTERN}$',
contentstore_views.export_git,
name='export_git')
]
if settings.FEATURES.get('ENABLE_SERVICE_STATUS'):
urlpatterns.append(path('status/', include('openedx.core.djangoapps.service_status.urls')))
# The password pages in the admin tool are disabled so that all password
# changes go through our user portal and follow complexity requirements.
if not settings.FEATURES.get('ENABLE_CHANGE_USER_PASSWORD_ADMIN'):
urlpatterns.append(re_path(r'^admin/auth/user/\d+/password/$', handler404))
urlpatterns.append(path('admin/password_change/', handler404))
urlpatterns.append(
path('admin/login/', contentstore_views.redirect_to_lms_login_for_admin, name='redirect_to_lms_login_for_admin')
)
urlpatterns.append(path('admin/', admin.site.urls))
# enable entrance exams
if core_toggles.ENTRANCE_EXAMS.is_enabled():
urlpatterns.append(re_path(fr'^course/{settings.COURSE_KEY_PATTERN}/entrance_exam/?$',
contentstore_views.entrance_exam))
# Enable Web/HTML Certificates
if settings.FEATURES.get('CERTIFICATES_HTML_VIEW'):
from cms.djangoapps.contentstore.views.certificates import (
CertificateActivationAPIView,
CertificateDetailAPIView,
certificates_list_handler,
signatory_detail_handler,
)
urlpatterns += [
re_path(fr'^certificates/activation/{settings.COURSE_KEY_PATTERN}/',
CertificateActivationAPIView.as_view(),
name='certificate_activation_handler'),
re_path(r'^certificates/{}/(?P<certificate_id>\d+)/signatories/(?P<signatory_id>\d+)?$'.format(
settings.COURSE_KEY_PATTERN), signatory_detail_handler, name='signatory_detail_handler'),
re_path(fr'^certificates/{settings.COURSE_KEY_PATTERN}/(?P<certificate_id>\d+)?$',
CertificateDetailAPIView.as_view(), name='certificates_detail_handler'),
re_path(fr'^certificates/{settings.COURSE_KEY_PATTERN}$',
certificates_list_handler, name='certificates_list_handler')
]
if settings.DEBUG:
try:
from .urls_dev import urlpatterns as dev_urlpatterns
urlpatterns += dev_urlpatterns
except ImportError:
pass
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
urlpatterns += static(
settings.VIDEO_IMAGE_SETTINGS['BASE_URL'],
document_root=settings.VIDEO_IMAGE_SETTINGS['STORAGE_KWARGS']['location']
)
urlpatterns += static(
settings.VIDEO_TRANSCRIPTS_SETTINGS['BASE_URL'],
document_root=settings.VIDEO_TRANSCRIPTS_SETTINGS['STORAGE_KWARGS']['location']
)
if 'debug_toolbar' in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns.append(path('__debug__/', include(debug_toolbar.urls)))
# UX reference templates
urlpatterns.append(path('template/<path:template>', openedx.core.djangoapps.debug.views.show_reference_template,
name='openedx.core.djangoapps.debug.views.show_reference_template'))
urlpatterns.append(
path('api/learning_sequences/', include(
('openedx.core.djangoapps.content.learning_sequences.urls', 'learning_sequences'),
namespace='learning_sequences'
),
),
)
urlpatterns.append(
path('', include(('openedx.core.djangoapps.content.search.urls', 'content_search'), namespace='content_search')),
)
# display error page templates, for testing purposes
urlpatterns += [
path('404', handler404),
path('500', handler500),
]
# API docs.
urlpatterns += make_docs_urls(api_info)
# edx-drf-extensions csrf app
urlpatterns += [
path('', include('csrf.urls')),
]
if 'openedx.testing.coverage_context_listener' in settings.INSTALLED_APPS:
urlpatterns += [
path('coverage_context', include('openedx.testing.coverage_context_listener.urls'))
]
# pylint: disable=wrong-import-position, wrong-import-order
from edx_django_utils.plugins import get_plugin_url_patterns # isort:skip
# pylint: disable=wrong-import-position
from openedx.core.djangoapps.plugins.constants import ProjectType # isort:skip
urlpatterns.extend(get_plugin_url_patterns(ProjectType.CMS))
# Contentstore REST APIs
urlpatterns += [
path('api/contentstore/', include('cms.djangoapps.contentstore.rest_api.urls'))
]
# Content tagging
urlpatterns += [
path('api/content_tagging/', include(('openedx.core.djangoapps.content_tagging.urls', 'content_tagging'))),
]
# Authoring-api specific API docs (using drf-spectacular and openapi-v3).
# This is separate from and in addition to the full studio swagger documentation already existing at /api-docs.
# Custom settings are provided in SPECTACULAR_SETTINGS as environment variables
# Filter function in cms/lib/spectacular.py determines paths that are swagger-documented.
urlpatterns += [
re_path('^authoring-api/ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'),
re_path('^authoring-api/schema/', SpectacularAPIView.as_view(), name='schema'),
]