Skip to content

Commit

Permalink
Merge pull request #46 from GuillaumeDerval/codemirror
Browse files Browse the repository at this point in the history
Improve studio; switch to Code Mirror for syntax highlighting
  • Loading branch information
GuillaumeDerval committed Apr 1, 2015
2 parents 36ecb2b + cc91232 commit 2c6f602
Show file tree
Hide file tree
Showing 567 changed files with 42,636 additions and 980 deletions.
2 changes: 1 addition & 1 deletion app_frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
'/admin/([^/]+)/tasks', 'frontend.pages.course_admin.task_list.CourseTaskListPage',
'/admin/([^/]+)/task/([^/]+)', 'frontend.pages.course_admin.task_info.CourseTaskInfoPage',
'/admin/([^/]+)/edit/([^/]+)', 'frontend.pages.course_admin.task_edit.CourseEditTask',
'/admin/([^/]+)/files/([^/]+)', 'frontend.pages.course_admin.task_file.DownloadTaskFiles',
'/admin/([^/]+)/edit/([^/]+)/files', 'frontend.pages.course_admin.task_edit_file.CourseTaskFiles',
'/admin/([^/]+)/submissions', 'frontend.pages.course_admin.submission_files.DownloadSubmissionFiles'
)

Expand Down
1 change: 1 addition & 0 deletions frontend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def get_template_renderer(dir_path, base=None):
return web.template.render(dir_path, globals=add_to_template_globals.globals, base=base)

renderer = get_template_renderer('templates/', 'layout')
add_to_template_globals.globals["include"] = get_template_renderer('templates/')


def new_database_client():
Expand Down
2 changes: 1 addition & 1 deletion frontend/pages/course_admin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,4 +95,4 @@ def POST(self, courseid):

def page(self, course, errors=None, saved=False):
""" Get all data and display the page """
return renderer.admin_course_settings(course, errors, saved)
return renderer.course_admin.settings(course, errors, saved)
2 changes: 1 addition & 1 deletion frontend/pages/course_admin/student_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ def page(self, course, username):
result[taskdata["taskid"]]["grade"] = taskdata["grade"]
if "csv" in web.input():
return make_csv(result)
return renderer.admin_course_student(course, username, result)
return renderer.course_admin.student(course, username, result)
2 changes: 1 addition & 1 deletion frontend/pages/course_admin/student_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ def page(self, course):
data = [dict(f.items() + [("url", self.submission_url_generator(course, username)), ("username", username)]) for username, f in data.iteritems()]
if "csv" in web.input():
return make_csv(data)
return renderer.admin_course_student_list(course, data)
return renderer.course_admin.student_list(course, data)
2 changes: 1 addition & 1 deletion frontend/pages/course_admin/student_task.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def page(self, course, username, task):
data = [dict(f.items() + [("url", self.submission_url_generator(course, str(f["_id"])))]) for f in data]
if "csv" in web.input():
return make_csv(data)
return renderer.admin_course_student_task(course, username, task, data)
return renderer.course_admin.student_task(course, username, task, data)


class SubmissionDownloadFeedback(object):
Expand Down
10 changes: 5 additions & 5 deletions frontend/pages/course_admin/task_edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,11 @@
import common.custom_yaml
from common.task_file_managers.manage import get_task_file_manager, get_available_task_file_managers, delete_all_possible_task_files
from frontend.accessible_time import AccessibleTime
from frontend.base import renderer
from frontend.base import renderer, get_template_renderer
from frontend.custom.courses import FrontendCourse
from frontend.custom.tasks import FrontendTask
from frontend.pages.course_admin.task_edit_file import CourseTaskFiles
from frontend.pages.course_admin.utils import get_course_and_check_rights


class CourseEditTask(object):

""" Edit a task """
Expand Down Expand Up @@ -72,7 +71,7 @@ def GET(self, courseid, taskid):
del problem_copy[i]
problem["custom"] = common.custom_yaml.dump(problem_copy)

return renderer.admin_course_edit_task(
return renderer.course_admin.edit_task(
course,
taskid,
task_data,
Expand All @@ -84,7 +83,8 @@ def GET(self, courseid, taskid):
self.contains_is_html(task_data),
current_filetype,
available_filetypes,
AccessibleTime)
AccessibleTime,
CourseTaskFiles.get_task_filelist(courseid, taskid))

@classmethod
def contains_is_html(cls, data):
Expand Down
283 changes: 283 additions & 0 deletions frontend/pages/course_admin/task_edit_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,283 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2014-2015 Université Catholique de Louvain.
#
# This file is part of INGInious.
#
# INGInious is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# INGInious is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with INGInious. If not, see <http://www.gnu.org/licenses/>.
""" Allow to create/edit/delete/move/download files associated to tasks """
import codecs
import json
import mimetypes
import os.path
import shutil
import tarfile
import tempfile

import web

from common.base import INGIniousConfiguration, id_checker
from common.task_file_managers.manage import get_available_task_file_managers
from frontend.base import get_template_renderer
from frontend.custom.courses import FrontendCourse
from frontend.pages.course_admin.utils import get_course_and_check_rights


class CourseTaskFiles(object):

""" Edit a task """

def GET(self, courseid, taskid):
""" Edit a task """
if not id_checker(taskid):
raise Exception("Invalid task id")

get_course_and_check_rights(courseid)

request = web.input()
if request.get("action") == "download" and request.get('path') is not None:
return self.action_download(courseid, taskid, request.get('path'))
elif request.get("action") == "delete" and request.get('path') is not None:
return self.action_delete(courseid, taskid, request.get('path'))
elif request.get("action") == "rename" and request.get('path') is not None and request.get('new_path') is not None:
return self.action_rename(courseid, taskid, request.get('path'), request.get('new_path'))
elif request.get("action") == "create" and request.get('path') is not None:
return self.action_create(courseid, taskid, request.get('path'))
elif request.get("action") == "edit" and request.get('path') is not None:
return self.action_edit(courseid, taskid, request.get('path'))
else:
return self.show_tab_file(courseid, taskid)

def POST(self, courseid, taskid):
""" Upload or modify a file """
if not id_checker(taskid):
raise Exception("Invalid task id")

get_course_and_check_rights(courseid)

request = web.input(file={})
if request.get("action") == "upload" and request.get('path') is not None and request.get('file') is not None:
return self.action_upload(courseid, taskid, request.get('path'), request.get('file'))
elif request.get("action") == "edit_save" and request.get('path') is not None and request.get('content') is not None:
return self.action_edit_save(courseid, taskid, request.get('path'), request.get('content'))
else:
return self.show_tab_file(courseid, taskid)

def show_tab_file(self, courseid, taskid, error=False):
""" Return the file tab """
return get_template_renderer('templates/').course_admin.edit_tabs.files(FrontendCourse(courseid), taskid, self.get_task_filelist(courseid, taskid))

@classmethod
def get_task_filelist(cls, courseid, taskid):
""" Returns a flattened version of all the files inside the task directory, excluding the files task.* and hidden files.
It returns a list of tuples, of the type (Integer Level, Boolean IsDirectory, String Name, String CompleteName)
"""
path = os.path.join(INGIniousConfiguration["tasks_directory"], courseid, taskid)
if not os.path.exists(path):
return []
result_dict = {}
for root, _, files in os.walk(path):
rel_root = os.path.normpath(os.path.relpath(root, path))
insert_dict = result_dict
if rel_root != ".":
hidden_dir = False
for i in rel_root.split(os.path.sep):
if i.startswith("."):
hidden_dir = True
break
if i not in insert_dict:
insert_dict[i] = {}
insert_dict = insert_dict[i]
if hidden_dir:
continue
for f in files:
# Do not follow symlinks and do not take into account task describers
if not os.path.islink(
os.path.join(
root, f)) and not (
root == path and os.path.splitext(f)[0] == "task" and os.path.splitext(f)[1][
1:] in get_available_task_file_managers().keys()) and not f.startswith("."):
insert_dict[f] = None

def recur_print(current, level, current_name):
iteritems = sorted(current.iteritems())
# First, the files
recur_print.flattened += [(level, False, f, os.path.join(current_name, f)) for f, t in iteritems if t is None]
# Then, the dirs
for name, sub in iteritems:
if sub is not None:
recur_print.flattened.append((level, True, name, os.path.join(current_name, name)))
recur_print(sub, level + 1, os.path.join(current_name, name))

recur_print.flattened = []
recur_print(result_dict, 0, '')
return recur_print.flattened

def verify_path(self, courseid, taskid, path, new_path=False):
""" Return the real wanted path (relative to the INGInious root) or None if the path is not valid/allowed """

task_dir_path = os.path.join(INGIniousConfiguration["tasks_directory"], courseid, taskid)
# verify that the dir exists
if not os.path.exists(task_dir_path):
return None
wanted_path = os.path.normpath(os.path.join(task_dir_path, path))
rel_wanted_path = os.path.relpath(wanted_path, task_dir_path) # normalized
# verify that the path we want exists and is withing the directory we want
if (new_path == os.path.exists(wanted_path)) or os.path.islink(wanted_path) or rel_wanted_path.startswith('..'):
return None
# do not allow touching the task.* file
if os.path.splitext(rel_wanted_path)[0] == "task" and os.path.splitext(rel_wanted_path)[1][1:] in get_available_task_file_managers().keys():
return None
# do not allow hidden dir/files
if rel_wanted_path != ".":
for i in rel_wanted_path.split(os.path.sep):
if i.startswith("."):
return None
return wanted_path

def action_edit(self, courseid, taskid, path):
""" Edit a file """
wanted_path = self.verify_path(courseid, taskid, path)
if wanted_path is None or not os.path.isfile(wanted_path):
return "Internal error"

content = open(wanted_path, 'r').read()
try:
content.decode('utf-8')
return json.dumps({"content": content})
except:
return json.dumps({"error": "not-readable"})

def action_edit_save(self, courseid, taskid, path, content):
""" Save an edited file """
wanted_path = self.verify_path(courseid, taskid, path)
if wanted_path is None or not os.path.isfile(wanted_path):
return "Internal error"

try:
with codecs.open(wanted_path, "w", "utf-8") as f:
f.write(content)
return json.dumps({"ok": True})
except:
return json.dumps({"error": True})

def action_upload(self, courseid, taskid, path, fileobj):
""" Upload a file """

wanted_path = self.verify_path(courseid, taskid, path, True)
if wanted_path is None:
return self.show_tab_file(courseid, taskid, "Invalid new path")
curpath = os.path.join(INGIniousConfiguration["tasks_directory"], courseid, taskid)
rel_path = os.path.relpath(wanted_path, curpath)

for i in rel_path.split(os.path.sep)[:-1]:
curpath = os.path.join(curpath, i)
if not os.path.exists(curpath):
os.mkdir(curpath)
if not os.path.isdir(curpath):
return self.show_tab_file(courseid, taskid, i + " is not a directory!")

try:
open(wanted_path, "w").write(fileobj.file.read())
return self.show_tab_file(courseid, taskid)
except:
return self.show_tab_file(courseid, taskid, "An error occurred while writing the file")

def action_create(self, courseid, taskid, path):
""" Delete a file or a directory """

want_directory = path.strip().endswith("/")

wanted_path = self.verify_path(courseid, taskid, path, True)
if wanted_path is None:
return self.show_tab_file(courseid, taskid, "Invalid new path")
curpath = os.path.join(INGIniousConfiguration["tasks_directory"], courseid, taskid)
rel_path = os.path.relpath(wanted_path, curpath)

for i in rel_path.split(os.path.sep)[:-1]:
curpath = os.path.join(curpath, i)
if not os.path.exists(curpath):
os.mkdir(curpath)
if not os.path.isdir(curpath):
return self.show_tab_file(courseid, taskid, i + " is not a directory!")
if rel_path.split(os.path.sep)[-1] != "":
if want_directory:
os.mkdir(os.path.join(curpath, rel_path.split(os.path.sep)[-1]))
else:
open(os.path.join(curpath, rel_path.split(os.path.sep)[-1]), 'a')
return self.show_tab_file(courseid, taskid)

def action_rename(self, courseid, taskid, path, new_path):
""" Delete a file or a directory """

old_path = self.verify_path(courseid, taskid, path)
if old_path is None:
return self.show_tab_file(courseid, taskid, "Internal error")

wanted_path = self.verify_path(courseid, taskid, new_path, True)
if wanted_path is None:
return self.show_tab_file(courseid, taskid, "Invalid new path")

try:
shutil.move(old_path, wanted_path)
return self.show_tab_file(courseid, taskid)
except:
return self.show_tab_file(courseid, taskid, "An error occurred while moving the files")

def action_delete(self, courseid, taskid, path):
""" Delete a file or a directory """

wanted_path = self.verify_path(courseid, taskid, path)
if wanted_path is None:
return self.show_tab_file(courseid, taskid, "Internal error")

# special case: cannot delete current directory of the task
if "." == os.path.relpath(wanted_path, os.path.join(INGIniousConfiguration["tasks_directory"], courseid, taskid)):
return self.show_tab_file(courseid, taskid, "Internal error")

if os.path.isdir(wanted_path):
shutil.rmtree(wanted_path)
else:
os.unlink(wanted_path)
return self.show_tab_file(courseid, taskid)

def action_download(self, courseid, taskid, path):
""" Download a file or a directory """

wanted_path = self.verify_path(courseid, taskid, path)
if wanted_path is None:
raise web.notfound()

# if the user want a dir:
if os.path.isdir(wanted_path):
tmpfile = tempfile.TemporaryFile()
tar = tarfile.open(fileobj=tmpfile, mode='w:gz')
for root, _, files in os.walk(wanted_path):
for fname in files:
info = tarfile.TarInfo(name=os.path.join(os.path.relpath(root, wanted_path), fname))
file_stat = os.stat(os.path.join(root, fname))
info.size = file_stat.st_size
info.mtime = file_stat.st_mtime
tar.addfile(info, fileobj=open(os.path.join(root, fname), 'r'))
tar.close()
tmpfile.seek(0)
web.header('Content-Type', 'application/x-gzip', unique=True)
web.header('Content-Disposition', 'attachment; filename="dir.tgz"', unique=True)
return tmpfile
else:
mimetypes.init()
mime_type = mimetypes.guess_type(wanted_path)
web.header('Content-Type', mime_type[0])
web.header('Content-Disposition', 'attachment; filename="' + os.path.split(wanted_path)[1] + '"', unique=True)
return open(wanted_path, 'r')

0 comments on commit 2c6f602

Please sign in to comment.