Source code for inginious.frontend.pages.course_admin.danger_zone

# -*- coding: utf-8 -*-
#
# This file is part of INGInious. See the LICENSE and the COPYRIGHTS files for
# more information about the licensing of this file.

import datetime
import glob
import hashlib
import logging
import os
import random
import zipfile
import bson.json_util
import flask

from flask import redirect, Response
from werkzeug.exceptions import NotFound
from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage


[docs]class CourseDangerZonePage(INGIniousAdminPage): """ Course administration page: list of audiences """ _logger = logging.getLogger("inginious.webapp.danger_zone")
[docs] def wipe_course(self, courseid): submissions = self.database.submissions.find({"courseid": courseid}) for submission in submissions: for key in ["input", "archive"]: gridfs = self.submission_manager.get_gridfs() if key in submission and type(submission[key]) == bson.objectid.ObjectId and gridfs.exists(submission[key]): gridfs.delete(submission[key]) self.database.courses.update_one({"_id": courseid}, {"$set": {"students": []}}) self.database.audiences.remove({"courseid": courseid}) self.database.groups.remove({"courseid": courseid}) self.database.user_tasks.remove({"courseid": courseid}) self.database.submissions.remove({"courseid": courseid}) self._logger.info("Course %s wiped.", courseid)
[docs] def dump_course(self, courseid): """ Create a zip file containing all information about a given course in database and then remove it from db""" filepath = os.path.join(self.backup_dir, courseid, datetime.datetime.now().strftime("%Y%m%d.%H%M%S") + ".zip") if not os.path.exists(os.path.dirname(filepath)): os.makedirs(os.path.dirname(filepath)) with zipfile.ZipFile(filepath, "w", allowZip64=True) as zipf: course_obj = self.database.courses.find_one({"_id": courseid}) students = course_obj.get("students", []) if course_obj else [] zipf.writestr("students.json", bson.json_util.dumps(students), zipfile.ZIP_DEFLATED) audiences = self.database.audiences.find({"courseid": courseid}) zipf.writestr("audiences.json", bson.json_util.dumps(audiences), zipfile.ZIP_DEFLATED) groups = self.database.groups.find({"courseid": courseid}) zipf.writestr("groups.json", bson.json_util.dumps(groups), zipfile.ZIP_DEFLATED) user_tasks = self.database.user_tasks.find({"courseid": courseid}) zipf.writestr("user_tasks.json", bson.json_util.dumps(user_tasks), zipfile.ZIP_DEFLATED) # Fetching input data while looping on submissions can trigger a mongo cursor timeout submissions = self.database.submissions.find({"courseid": courseid}, no_cursor_timeout=True) erroneous_subs = set() for submission in submissions: for key in ["input", "archive"]: gridfs = self.submission_manager.get_gridfs() if key in submission and type(submission[key]) == bson.objectid.ObjectId: if gridfs.exists(submission[key]): infile = gridfs.get(submission[key]) zipf.writestr(key + "/" + str(submission[key]) + ".data", infile.read(), zipfile.ZIP_DEFLATED) else: self._logger.error("Missing {} in grifs, skipping submission {}".format(str(submission[key]), str(submission["_id"]))) erroneous_subs.add(submission["_id"]) submissions.rewind() submissions = [submission for submission in submissions if submission["_id"] not in erroneous_subs] zipf.writestr("submissions.json", bson.json_util.dumps(submissions), zipfile.ZIP_DEFLATED) self._logger.info("Course %s dumped to backup directory.", courseid) self.wipe_course(courseid)
[docs] def restore_course(self, courseid, backup): """ Restores a course of given courseid to a date specified in backup (format : YYYYMMDD.HHMMSS) """ self.wipe_course(courseid) filepath = os.path.join(self.backup_dir, courseid, backup + ".zip") with zipfile.ZipFile(filepath, "r") as zipf: students = bson.json_util.loads(zipf.read("students.json").decode("utf-8")) if len(students) > 0: self.database.courses.update_one({"_id": courseid}, {"$set": {"students": students}}, upsert=True) audiences = bson.json_util.loads(zipf.read("audiences.json").decode("utf-8")) if len(audiences) > 0: self.database.audiences.insert(audiences) groups = bson.json_util.loads(zipf.read("groups.json").decode("utf-8")) if len(groups) > 0: self.database.groups.insert(groups) user_tasks = bson.json_util.loads(zipf.read("user_tasks.json").decode("utf-8")) if len(user_tasks) > 0: self.database.user_tasks.insert(user_tasks) submissions = bson.json_util.loads(zipf.read("submissions.json").decode("utf-8")) for submission in submissions: for key in ["input", "archive"]: if key in submission and type(submission[key]) == bson.objectid.ObjectId: submission[key] = self.submission_manager.get_gridfs().put(zipf.read(key + "/" + str(submission[key]) + ".data")) if len(submissions) > 0: self.database.submissions.insert(submissions) self._logger.info("Course %s restored from backup directory.", courseid)
[docs] def delete_course(self, courseid): """ Erase all course data """ # Wipes the course (delete database) self.wipe_course(courseid) # Deletes the course from the factory (entire folder) self.course_factory.delete_course(courseid) # Removes backup filepath = os.path.join(self.backup_dir, courseid) if os.path.exists(os.path.dirname(filepath)): for backup in glob.glob(os.path.join(filepath, '*.zip')): os.remove(backup) self._logger.info("Course %s files erased.", courseid)
[docs] def GET_AUTH(self, courseid): # pylint: disable=arguments-differ """ GET request """ course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False) data = flask.request.args if "download" in data: filepath = os.path.join(self.backup_dir, courseid, data["download"] + '.zip') if not os.path.exists(os.path.dirname(filepath)): raise NotFound(description=_("This file doesn't exist.")) response = Response(response=open(filepath, 'rb'), content_type='application/zip') response.headers['Content-Disposition'] = 'attachment; filename="{}.zip"'.format(data["download"]) return response else: return self.page(course)
[docs] def POST_AUTH(self, courseid): # pylint: disable=arguments-differ """ POST request """ course, __ = self.get_course_and_check_rights(courseid, allow_all_staff=False) msg = "" error = False data = flask.request.form if not data.get("token", "") == self.user_manager.session_token(): msg = _("Operation aborted due to invalid token.") error = True elif "wipeall" in data: if not data.get("courseid", "") == courseid: msg = _("Wrong course id.") error = True else: try: self.dump_course(courseid) msg = _("All course data have been deleted.") except Exception as ex: msg = _("An error occurred while dumping course from database: {}").format(repr(ex)) error = True elif "restore" in data: if "backupdate" not in data: msg = "No backup date selected." error = True else: try: dt = datetime.datetime.strptime(data["backupdate"], "%Y%m%d.%H%M%S") self.restore_course(courseid, data["backupdate"]) msg = _("Course restored to date : {}.").format(dt.strftime("%Y-%m-%d %H:%M:%S")) except Exception as ex: msg = _("An error occurred while restoring backup: {}").format(repr(ex)) error = True elif "deleteall" in data: if not data.get("courseid", "") == courseid: msg = _("Wrong course id.") error = True else: try: self.delete_course(courseid) return redirect(self.app.get_homepath() + '/index') except Exception as ex: msg = _("An error occurred while deleting the course data: {}").format(repr(ex)) error = True return self.page(course, msg, error)
[docs] def get_backup_list(self, course): backups = [] filepath = os.path.join(self.backup_dir, course.get_id()) if os.path.exists(os.path.dirname(filepath)): for backup in glob.glob(os.path.join(filepath, '*.zip')): try: basename = os.path.basename(backup)[0:-4] dt = datetime.datetime.strptime(basename, "%Y%m%d.%H%M%S").strftime("%Y-%m-%d %H:%M:%S") backups.append({"file": basename, "date": dt}) except: # Wrong format pass return backups
[docs] def page(self, course, msg="", error=False): """ Get all data and display the page """ thehash = hashlib.sha512(str(random.getrandbits(256)).encode("utf-8")).hexdigest() self.user_manager.set_session_token(thehash) backups = self.get_backup_list(course) return self.template_helper.render("course_admin/danger_zone.html", course=course, thehash=thehash, backups=backups, msg=msg, error=error)