""" Views for the maintenance app. """ import logging from django.core.validators import ValidationError from django.db import transaction from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ from django.views.generic import View from django.views.generic.edit import CreateView, DeleteView, UpdateView from django.views.generic.list import ListView from opaque_keys import InvalidKeyError from opaque_keys.edx.keys import CourseKey from cms.djangoapps.contentstore.management.commands.utils import get_course_versions from common.djangoapps.edxmako.shortcuts import render_to_response from common.djangoapps.util.json_request import JsonResponse from common.djangoapps.util.views import require_global_staff from openedx.features.announcements.forms import AnnouncementForm from openedx.features.announcements.models import Announcement from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError log = logging.getLogger(__name__) # This dict maintains all the views that will be used Maintenance app. MAINTENANCE_VIEWS = { 'force_publish_course': { 'url': 'maintenance:force_publish_course', 'name': _('Force Publish Course'), 'slug': 'force_publish_course', 'description': _( 'Sometimes the draft and published branches of a course can get out of sync. Force publish course command ' 'resets the published branch of a course to point to the draft branch, effectively force publishing the ' 'course. This view dry runs the force publish command' ), }, 'announcement_index': { 'url': 'maintenance:announcement_index', 'name': _('Edit Announcements'), 'slug': 'announcement_index', 'description': _( 'This view shows the announcement editor to create or alter announcements that are shown on the right' 'side of the dashboard.' ), }, } COURSE_KEY_ERROR_MESSAGES = { 'empty_course_key': _('Please provide course id.'), 'invalid_course_key': _('Invalid course key.'), 'course_key_not_found': _('No matching course found.') } class MaintenanceIndexView(View): """ Index view for maintenance dashboard, used by global staff. This view lists some commands/tasks that can be used to dry run or execute directly. """ @method_decorator(require_global_staff) def get(self, request): """Render the maintenance index view. """ return render_to_response('maintenance/index.html', { 'views': MAINTENANCE_VIEWS, }) class MaintenanceBaseView(View): """ Base class for Maintenance views. """ template = 'maintenance/container.html' def __init__(self, view=None): super().__init__() self.context = { 'view': view if view else '', 'form_data': {}, 'error': False, 'msg': '' } def render_response(self): """ A short method to render_to_response that renders response. """ if self.request.is_ajax(): return JsonResponse(self.context) return render_to_response(self.template, self.context) @method_decorator(require_global_staff) def get(self, request): """ Render get view. """ return self.render_response() def validate_course_key(self, course_key, branch=ModuleStoreEnum.BranchName.draft): """ Validates the course_key that would be used by maintenance app views. Arguments: course_key (string): a course key branch: a course locator branch, default value is ModuleStoreEnum.BranchName.draft . values can be either ModuleStoreEnum.BranchName.draft or ModuleStoreEnum.BranchName.published. Returns: course_usage_key (CourseLocator): course usage locator """ if not course_key: raise ValidationError(COURSE_KEY_ERROR_MESSAGES['empty_course_key']) course_usage_key = CourseKey.from_string(course_key) if not modulestore().has_course(course_usage_key): raise ItemNotFoundError(COURSE_KEY_ERROR_MESSAGES['course_key_not_found']) # get branch specific locator course_usage_key = course_usage_key.for_branch(branch) return course_usage_key class ForcePublishCourseView(MaintenanceBaseView): """ View for force publishing state of the course, used by the global staff. This view uses `force_publish_course` method of modulestore which publishes the draft state of the course. After the course has been forced published, both draft and publish draft point to same location. """ def __init__(self): super().__init__(MAINTENANCE_VIEWS['force_publish_course']) self.context.update({ 'current_versions': [], 'updated_versions': [], 'form_data': { 'course_id': '', 'is_dry_run': True } }) def get_course_branch_versions(self, versions): """ Returns a dict containing unicoded values of draft and published draft versions. """ return { 'draft-branch': str(versions['draft-branch']), 'published-branch': str(versions['published-branch']) } @transaction.atomic @method_decorator(require_global_staff) def post(self, request): """ This method force publishes a course if dry-run argument is not selected. If dry-run is selected, this view shows possible outcome if the `force_publish_course` modulestore method is executed. Arguments: course_id (string): a request parameter containing course id is_dry_run (string): a request parameter containing dry run value. It is obtained from checkbox so it has either values 'on' or ''. """ course_id = request.POST.get('course-id') self.context.update({ 'form_data': { 'course_id': course_id } }) try: course_usage_key = self.validate_course_key(course_id) except InvalidKeyError: self.context['error'] = True self.context['msg'] = COURSE_KEY_ERROR_MESSAGES['invalid_course_key'] except ItemNotFoundError as exc: self.context['error'] = True self.context['msg'] = str(exc) except ValidationError as exc: self.context['error'] = True self.context['msg'] = str(exc) if self.context['error']: return self.render_response() source_store = modulestore()._get_modulestore_for_courselike(course_usage_key) # pylint: disable=protected-access if not hasattr(source_store, 'force_publish_course'): self.context['msg'] = _('Force publishing course is not supported with old mongo courses.') log.warning( 'Force publishing course is not supported with old mongo courses. \ %s attempted to force publish the course %s.', request.user, course_id, exc_info=True ) return self.render_response() current_versions = self.get_course_branch_versions(get_course_versions(course_id)) # if publish and draft are NOT different if current_versions['published-branch'] == current_versions['draft-branch']: self.context['msg'] = _('Course is already in published state.') log.warning( 'Course is already in published state. %s attempted to force publish the course %s.', request.user, course_id, exc_info=True ) return self.render_response() self.context['current_versions'] = current_versions log.info( '%s dry ran force publish the course %s.', request.user, course_id, exc_info=True ) return self.render_response() class AnnouncementBaseView(View): """ Base view for Announcements pages """ @method_decorator(require_global_staff) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) class AnnouncementIndexView(ListView, MaintenanceBaseView): """ View for viewing the announcements shown on the dashboard, used by the global staff. """ model = Announcement object_list = Announcement.objects.order_by('-active') context_object_name = 'announcement_list' paginate_by = 8 def __init__(self): super().__init__(MAINTENANCE_VIEWS['announcement_index']) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['view'] = MAINTENANCE_VIEWS['announcement_index'] return context @method_decorator(require_global_staff) def get(self, request, *args, **kwargs): context = self.get_context_data() return render_to_response(self.template, context) class AnnouncementEditView(UpdateView, AnnouncementBaseView): """ View for editing an announcement. """ model = Announcement form_class = AnnouncementForm success_url = reverse_lazy('maintenance:announcement_index') template_name = '/maintenance/_announcement_edit.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['action_url'] = reverse('maintenance:announcement_edit', kwargs={'pk': context['announcement'].pk}) return context class AnnouncementCreateView(CreateView, AnnouncementBaseView): """ View for creating an announcement. """ model = Announcement form_class = AnnouncementForm success_url = reverse_lazy('maintenance:announcement_index') template_name = '/maintenance/_announcement_edit.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['action_url'] = reverse('maintenance:announcement_create') return context class AnnouncementDeleteView(DeleteView, AnnouncementBaseView): """ View for deleting an announcement. """ model = Announcement success_url = reverse_lazy('maintenance:announcement_index') template_name = '/maintenance/_announcement_delete.html'