diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d3f3baf --- /dev/null +++ b/.gitignore @@ -0,0 +1,262 @@ +# Created by https://www.toptal.com/developers/gitignore/api/python,flask +# Edit at https://www.toptal.com/developers/gitignore?templates=python,flask + +### Flask ### +instance/* +!instance/.gitignore +.webassets-cache +.env + +### Flask.Python Stack ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python ### +# Byte-compiled / optimized / DLL files + +# C extensions + +# Distribution / packaging + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. + +# Installer logs + +# Unit test / coverage reports + +# Translations + +# Django stuff: + +# Flask stuff: + +# Scrapy stuff: + +# Sphinx documentation + +# PyBuilder + +# Jupyter Notebook + +# IPython + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm + +# Celery stuff + +# SageMath parsed files + +# Environments + +# Spyder project settings + +# Rope project settings + +# mkdocs documentation + +# mypy + +# Pyre type checker + +# pytype static type analyzer + +# Cython debug symbols + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python,flask \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 4b91109..73b22a5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1 @@ -recursive-include inginious-jplag/templates/ * +recursive-include templates/ * diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..080db75 --- /dev/null +++ b/__init__.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +# +# Copyright (c) 2017 Ludovic Taffin +# This program 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. +# +# This program 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 this program. If not, see . + +# JPLAG plugin for INGInious +import os +import logging +import tempfile +import base64 +import datetime + +from flask import request, render_template, send_file +from werkzeug.exceptions import NotFound + +from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage +from inginious.frontend.courses import Course +from inginious.frontend.models import Submission +from inginious.client.client_sync import ClientSync + +""" A plugin that uses JPlag to detect similar code """ + +PATH_TO_PLUGIN = os.path.abspath(os.path.dirname(__file__)) +PATH_TO_TEMPLATES = os.path.join(PATH_TO_PLUGIN, "templates") + + +def add_admin_menu(course): # pylint: disable=unused-argument + """ Add a menu for jplag analyze in the administration """ + return ('jplagselectpage', '  JPlag') + + +def init(plugin_manager, client, config): + """ Init the plugin """ + + _logger = logging.getLogger("inginious.webapp.plugins.jplagselectpage") + + jplag_languages = { + "Java": "java", + "C": "c", + "C++": "cpp", + "C#": "csharp", + "Python": "python3", + "JavaScript": "javascript", + "TypeScript": "typescript", + "Go": "golang", + "Kotlin": "kotlin", + "R": "rlang", + "Rust": "rust", + "Swift": "swift", + "Scala": "scala", + "LLVM IR": "llvmir", + "Scheme": "scheme", + "EMF Metamodel": "emf", + "EMF Model": "emf-model", + "SCXML": "scxml", + "Text": "text", + "Multi-language": "multi", + } + + + class JPLAGSelectPage(INGIniousAdminPage): + + def GET_AUTH(self, courseid): + """GET REQUEST""" + _logger.info("Starting JPlag selection") + try: + course = Course.get(courseid) + except: + raise NotFound(description=_("Course not found.")) + + tasks = course.get_tasks() + _logger.info("Rendering task selection") + return render_template("jplag/templates/jplag_select_task.html", template_folder=PATH_TO_TEMPLATES, + course=courseid, tasks=tasks) + + class JPLAGPage(INGIniousAdminPage): + """ A JPlag page """ + + def GET_AUTH(self, courseid, taskid): + """GET REQUEST""" + + subs_qs = Submission.objects(courseid=courseid, taskid=taskid).order_by('submitted_on') + subs = [s.to_mongo().to_dict() for s in subs_qs] + + eval_mode = Course.get(courseid).get_task_dispenser().get_evaluation_mode(taskid) + if eval_mode == "last": + res = {elem['username'][0]: elem for elem in subs} + elif eval_mode == "best": + res = {} + for elem in subs: + username = elem['username'][0] + if username not in res or elem['grade'] > res[username]['grade']: + res[username] = elem + + return render_template("jplag/templates/jplagselector.html", template_folder=PATH_TO_TEMPLATES, + subs=res, languages=jplag_languages) + + def POST_AUTH(self, courseid, taskid): + """ POST REQUEST """ + + client_sync = ClientSync(client) + job_info = { "environment_type": "docker", "environment": "jplag"} + + selected_submission_ids = request.form.getlist("submissions") + language = request.form.get("language", None) + try: + percentage_raw = request.form.get("percentage", "70") + percentage = max(0, min(100, int(percentage_raw))) # clamp to [0, 100] + except ValueError: + percentage = 70 + + compare_with_archives = request.form.get("addarchives", "0") == "1" + + # get the input or archive for the selected submissions + subs = [self.submission_manager.get_submission(x, False) for x in selected_submission_ids] + course = Course.get(courseid) + raw_archive = self.submission_manager.get_submission_archive(course, subs, ["username"], simplify=True) + + submissions_bytes = raw_archive[0].read() + exclude_bytes = request.files.get("exclude").read() if "exclude" in request.files else b"" + additional_submissions_bytes = request.files.get("archi").read() if "archi" in request.files else b"" + template_bytes = request.files.get("template").read() if "template" in request.files else b"" + + job_input = { + 'submissions': base64.b64encode(submissions_bytes).decode('utf-8'), # tar archive of all submissions # TODO fix : not correct format because cannot be serialized by ZeroMQ, need to pass as bytes or file-like object + 'language': language, + 'similarity_threshold': percentage, + 'check_archives': compare_with_archives, + 'exclude': base64.b64encode(exclude_bytes).decode('utf-8'), + 'additional_submissions': base64.b64encode(additional_submissions_bytes).decode('utf-8'), + 'template': base64.b64encode(template_bytes).decode('utf-8'), + } + + result, grade, problems, tests, custom, state, archive, stdout, stderr = client_sync.new_job(0, + job_info=job_info, + inputdata=job_input, + launcher_name="Plugin - JPLAG", + debug=True) + + # Save archive bytes to a temp file + tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".zip", dir="/tmp") + tmp.write(archive) + tmp.close() + + + + + return render_template("jplag/templates/jplagresult.html", template_folder=PATH_TO_TEMPLATES, + download_token=os.path.basename(tmp.name), courseid=courseid, taskid=taskid) + + + class JPLAGDownloadPage(INGIniousAdminPage): + def GET_AUTH(self, filename, courseid, taskid): + + # TODO : check permissions + + # TODO : send zip instead of jplag + + # TODO : delete temp file after download or after some time + + + path = os.path.join("/tmp", filename) + if not os.path.exists(path): + raise NotFound() + return send_file(path, as_attachment=True, download_name=f'jplag_results_{courseid}_{taskid}_{datetime.datetime.now()}.tar.gz') + + plugin_manager.add_page("/jplag//", JPLAGPage.as_view("jplagpage")) + plugin_manager.add_page("/jplagselecttask//", JPLAGSelectPage.as_view("jplagselectpage")) + plugin_manager.add_page("/jplag/download///", JPLAGDownloadPage.as_view("jplagdownload")) + plugin_manager.add_hook('course_admin_menu', add_admin_menu) + plugin_manager.add_template_prefix("jplag", PATH_TO_PLUGIN) diff --git a/config.yaml b/config.yaml deleted file mode 100644 index 2eaf779..0000000 --- a/config.yaml +++ /dev/null @@ -1,3 +0,0 @@ -jplag: - name: 'jplag-2.11.9-SNAPSHOT-jar-with-dependencies.jar' - localisation: '.' \ No newline at end of file diff --git a/container/Dockerfile b/container/Dockerfile new file mode 100644 index 0000000..f58b1ba --- /dev/null +++ b/container/Dockerfile @@ -0,0 +1,37 @@ +# DOCKER-VERSION 1.1.0 + +# Environment variables for the JPLAG environment +# JPLAG_LANGUAGE (required) : The programming language to be used for the plagiarism detection. Supported values are available at https://github.com/jplag/JPlag. Be aware of the version used. +# SIM_THRESHOLD : The similarity threshold for the plagiarism detection. Default is 0 (0% similarity). + + +ARG VERSION=latest +ARG REGISTRY=ghcr.io +FROM ${REGISTRY}/inginious/env-base:${VERSION} + +LABEL org.opencontainers.image.source=https://github.com/INGInious/containers +LABEL org.opencontainers.image.description="Single test JPLAG job environment" +LABEL org.inginious.grading.name="jplag" +LABEL org.inginious.grading.advertise="false" + + +# Install Java SE 21 +RUN dnf clean metadata && \ + dnf -y upgrade && \ + dnf -y install java-21-openjdk-headless + + +# JPlag 6.2.0, last version compatible with Java 21 +ENV JPLAG_VERSION=6.2.0 +ENV JPLAG_JAR=/opt/jplag/jplag-${JPLAG_VERSION}-jar-with-dependencies.jar + +RUN mkdir -p /opt/jplag && \ + curl -L -o "${JPLAG_JAR}" \ + "https://github.com/jplag/JPlag/releases/download/v${JPLAG_VERSION}/jplag-${JPLAG_VERSION}-jar-with-dependencies.jar" + + +RUN mkdir -p /jplag/input /jplag/submissions /jplag/result /jplag/basecode && \ + chmod -R 777 /jplag + + +COPY run.py /course/run.py diff --git a/container/run.py b/container/run.py new file mode 100644 index 0000000..a8dbac0 --- /dev/null +++ b/container/run.py @@ -0,0 +1,175 @@ +from inginious_container_api import feedback, input +import sys + +import datetime +import shutil +import subprocess +import tarfile + +import re +import zipfile +import base64 + +import yaml +import os +import io + + +# ─── Paths ──────────────────────────────────────────────────────────────────── +# +# SUBMISSIONS_DIR : path to the directory where the submissions will be extracted +# RESULT_FILE : path to the JPlag result file (without extension, JPlag will add .jplag) +# BASE_CODE_DIR : path to the directory containing the base code/template code, the common framework used in all +# submissions (optional, only if you want to use +# EXCLUSION_FILE : path to the file containing the list of files to exclude from the analysis (optional, only if you want to use it) + +SUBMISSIONS_DIR = "/jplag/submissions" +RESULT_FILE = "/archive/results.jplag" +BASE_CODE_DIR = "/jplag/basecode" +EXCLUSION_FILE = "/jplag/exclude.txt" + + +def extract_tar_files_from_bytes(source, desti): + # Extracting submissions to the extract folder + f = open('sub.tar', 'wb') + f.write(source) + f.close() + file_like_object = io.BytesIO(source) + ar = tarfile.open(fileobj=file_like_object) + ar.extractall(path=desti) + ar.close() + +# ─── The submission tree ─────────────────────────────────────────── +# +# The submission tree given by get_submission_archive() is a tar archive with the following structure: +# +# one folder per student: +# submissions +# ├── alice/ +# │ ├── archive +# │ │ ├── file1.py +# │ │ └── ... +# │ ├── uploaded_files/ +# │ │ ├── student_file1.py +# │ │ └── ... +# │ └── submission.test +# ├── bob/ +# │ ├── ... +# │ └── ... +# └── charlie/ +# │ ├── ... +# │ └── ... +# ... +# +# submission.test contains the submission's metadata. It will be automatically ignored by JPLAG due to it's extension not +# matching with the selected language. +# The uploaded_files folder contains the files uploaded by the student for a file problem. +# The archive folder contains the files put inside the /archive directory during the submission execution. If you want +# classic input code problems to be analyzed by JPLAG, you need to save them in the /archive directory during the execution. + + + +# ─── 1. Checking the parameters ──────────────────────────────────────────────── +# +# submissions_raw_input : base64 encoded tar archive of all submissions (required) +# language : Language for which JPLAG will do it's analysis (required) +# sim_threshold : minimum similarity to store -m (default: 70%) +# check_archives : check the archives of the submissions (default: False) # TODO : remove ? Check the archive by default ? +# exclusion_file : base64 encoded tar archive of the file containing the list of files to exclude from the analysis (optional) +# additional_submissions : base64 encoded tar archive of additional submissions (eg. previous year), (optional) # TODO : check structure -> downloaded archive will not have the same structure +# template : base64 encoded tar archive of the template code, the one that will be common for each submission (optional) + +submissions_raw_input = base64.b64decode(input.get_input("submissions")) +language = input.get_input("language") +sim_threshold = str(float(input.get_input("similarity_threshold")) / 100) +check_archives = input.get_input("check_archives") +exclusion_file = base64.b64decode(input.get_input("exclude")) +additional_submissions = base64.b64decode(input.get_input("additional_submissions")) +template = base64.b64decode(input.get_input("template")) + + +# TODO : checking the values + + + +# Extracting submissions to the extract folder +extract_tar_files_from_bytes(submissions_raw_input, SUBMISSIONS_DIR) + +# Fill in template folder +if template: + extract_tar_files_from_bytes(template, BASE_CODE_DIR) + +# Getting old and added submissions in the extract +if additional_submissions: + extract_tar_files_from_bytes(additional_submissions, SUBMISSIONS_DIR) + +# Save exclude file +if exclusion_file: + extract_tar_files_from_bytes(exclusion_file, EXCLUSION_FILE) + +# Check if submitted files are zip file or code files +for root, subdirs, files in os.walk(SUBMISSIONS_DIR): + if root.endswith("uploaded_files"): + for elem in files: + if elem.endswith("zip"): + myzip = zipfile.ZipFile(root + "/" + elem) + try: + for f in myzip.namelist(): + myzip.open(f) + myzip.extract(f, path=root) + except zipfile.BadZipfile: + print("exception") + myzip.close() + elif elem.endswith(".tar.gz") or elem.endswith(".tgz"): + mytar = tarfile.open(root + "/" + elem) + mytar.extractall(path=root) + mytar.close() + # if we don't check the archives, we can remove the old submissions from the extract folder # TODO : check if archive correctly removed + if not check_archives: + for subdir in subdirs: + if subdir.startswith("archive_"): + shutil.rmtree(root + "/" + subdir) + + + +# ─── 2. Build the JPlag command ──────────────────────────────────────────────── + +os.makedirs(os.path.dirname(RESULT_FILE), exist_ok=True) + +JAVA_JAR = os.environ["JPLAG_JAR"] + +cmd = [ + "java", "-jar", JAVA_JAR, + "--mode", "run", # not launching the GUI viewer + "-l", language, + "-m", sim_threshold, + "-r", RESULT_FILE, + "--overwrite", +] + +# TODO : check if these are really None or empty bytes +if template: + print("Template code provided, using it as base code for JPlag.") + cmd += ["-bc", BASE_CODE_DIR] +if exclusion_file: + print("Exclusion file provided, using it to exclude files from the analysis.") + cmd += ["-x", EXCLUSION_FILE] + +cmd.append(SUBMISSIONS_DIR) + +# ─── 3. Run JPlag ────────────────────────────────────────────────────────────── +print(cmd) + +proc = subprocess.run(cmd, capture_output=True, text=True) + +if proc.returncode != 0: + print("failling running JPlag") + print(proc.stdout) + print(proc.stderr) + feedback.set_global_result("failed") + feedback.set_global_feedback(proc.stderr) + sys.exit(1) + +print("JPlag executed successfully.") + +feedback.set_global_result("success") diff --git a/inginious-jplag/__init__.py b/inginious-jplag/__init__.py deleted file mode 100644 index 05166f1..0000000 --- a/inginious-jplag/__init__.py +++ /dev/null @@ -1,136 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (c) 2017 Ludovic Taffin -# This program 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. -# -# This program 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 this program. If not, see . - -# JPLAG plugin for INGInious -import os -import logging -import requests -import pymongo -import tarfile - -from flask import request -from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage - -""" A plugin that uses JPlag to detect similar code """ - -PATH_TO_PLUGIN = os.path.abspath(os.path.dirname(__file__)) -PATH_TO_TEMPLATES = os.path.join(PATH_TO_PLUGIN, "templates") - - -def add_admin_menu(course): # pylint: disable=unused-argument - """ Add a menu for jplag analyze in the administration """ - return ('../../jplagselecttask/' + course.get_id() + '/', '  JPlag') - - -def get_submission_archive(submissions, gridfs, archive_file): - file_to_put = {} - for submission in submissions: - # generate all paths where the submission must belong - for base_path in submission["username"]: - path, i = base_path, 1 - while path in file_to_put: - path = base_path + "-" + str(i) - i += 1 - file_to_put[path] = submission - - tar = tarfile.open(fileobj=archive_file, mode='w:gz') - - for base_path, submission in file_to_put.items(): - # If there is an archive, add it too - if 'archive' in submission and submission['archive'] is not None and submission['archive'] != "": - subfile = gridfs.get(submission['archive']) - subtar = tarfile.open(fileobj=subfile, mode="r:gz") - - for member in subtar.getmembers(): - subtarfile = subtar.extractfile(member) - member.name = base_path + "/archive/" + member.name - tar.addfile(member, subtarfile) - - subtar.close() - subfile.close() - - # Close tarfile and put tempfile cursor at 0 - tar.close() - archive_file.seek(0) - return archive_file - - -def init(plugin_manager, _, _2, config): - """ Init the plugin """ - server = config.get('host') - port = config.get('port') - path = config.get('path') - _logger = logging.getLogger("inginious.webapp.plugins.jplagselectpage") - - class JPLAGSelectPage(INGIniousAdminPage): - - def GET_AUTH(self, courseID): - """GET REQUEST""" - _logger.info("Starting JPlag selection") - c = self.course_factory.get_course(courseID) - tasks = c.get_tasks() - _logger.info("Rendering task selection") - return self.template_helper.render("jplag_select_task.html", template_folder=PATH_TO_TEMPLATES, - course=courseID, tasks=tasks) - - class JPLAGPage(INGIniousAdminPage): - """ A JPlag page """ - - def GET_AUTH(self, courseID, taskID): - """GET REQUEST""" - - subs = list(self.database.submissions.find({"courseid": courseID, "taskid": taskID}).sort( - [("submitted_on", pymongo.ASCENDING)])) - res = {elem['username'][0]: elem for elem in subs} - return self.template_helper.render("jplagselector.html", template_folder=PATH_TO_TEMPLATES, subs=res) - - def POST_AUTH(self, courseID, taskID): - """ POST REQUEST """ - list_of_sub_id = [] - x = request.form - template_file = request.files['templateFile'] - old_sub_file = request.files['archiveFile'] - exclude_file = request.files['excludeFile'] - for key in x: - value = x[key] - if value == "on" and key != 'nextyear' and key != 'addarchives': - list_of_sub_id.append(key) - - subs = [self.submission_manager.get_submission(x, False) for x in list_of_sub_id] - archive = get_submission_archive(subs, self.app.gridfs, open(self.app.backup_dir + "/test.tar", "wb+")) - - r = requests.post('http://' + server + ':' + str(port) + '/' + path, - files={'sub.tar': archive, - 'courseId': courseID, - 'taskId': taskID, - 'lang': x['selectLang'] if 'selectLang' in x else 'Python', - 'template': template_file.read() if template_file is not None else '', - 'archi': old_sub_file.read() if old_sub_file is not None else '', - 'exclude': exclude_file.read() if exclude_file is not None else '', - 'percentage': x['percentage'] if 'percentage' in x else 1, - 'nyear': x['nextyear'] if 'nextyear' in x else "no", - 'check_archives': x['addarchives'] if 'addarchives' in x else "no"}) - if r.status_code == 200: - return self.template_helper.render("jplagresult.html", template_folder=PATH_TO_TEMPLATES, - url=r.content.decode('UTF-8')) - else: - return r.content.decode('utf-8') - - archive.close() - - plugin_manager.add_page("/jplag//", JPLAGPage.as_view("jplagpage")) - plugin_manager.add_page("/jplagselecttask//", JPLAGSelectPage.as_view("jplagselectpage")) - plugin_manager.add_hook('course_admin_menu', add_admin_menu) diff --git a/inginious-jplag/templates/jplagresult.html b/inginious-jplag/templates/jplagresult.html deleted file mode 100644 index 7210f82..0000000 --- a/inginious-jplag/templates/jplagresult.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "layout.html" %} -{% block title %} JPlag - Analyze of submissions {% endblock %} - -{% block content %} - -

Pay attention that results are only available from inside of UCLouvain network.

- -
- Results -
- -{% endblock %} \ No newline at end of file diff --git a/inginious-jplag/templates/jplagselector.html b/inginious-jplag/templates/jplagselector.html deleted file mode 100644 index 8a1cc91..0000000 --- a/inginious-jplag/templates/jplagselector.html +++ /dev/null @@ -1,88 +0,0 @@ -{% extends "layout.html" %} -{% block title %} JPlag - Analysis of submissions {% endblock %} - -{% block content %} -

Please select submissions to analyze.

- -
- - -
- -
-
    - {% for k,v in subs.items() %} -
  • - {% endfor %} -
-

Select code language

- - -

Insert a template archive (tar)

- - -

Insert an archive of extra old submissions (tar)

- - -

Insert an exclude file (txt) -

- - -

Percentage -

- - -

Save submissions files for next year? -

- - -

Compare with archives? -

- - - -
- - - - -{% endblock %} \ No newline at end of file diff --git a/jplag-2.11.9-SNAPSHOT-jar-with-dependencies.jar b/jplag-2.11.9-SNAPSHOT-jar-with-dependencies.jar deleted file mode 100644 index ea1977f..0000000 Binary files a/jplag-2.11.9-SNAPSHOT-jar-with-dependencies.jar and /dev/null differ diff --git a/setup.py b/setup.py index 27ebbc9..174c2ce 100644 --- a/setup.py +++ b/setup.py @@ -20,13 +20,12 @@ retval = setup( name='inginious-jplag', - version="0.1", - author="Ludovic Taffin", - author_email = "ludovic.taffin@uclouvain.be", + version="0.2", + author="Ludovic Taffin, Alexandre Doneux", packages=find_packages(), - url="http://www.uclouvain.be", + url="https://github.com/INGInious/INGInious", license = "GNU", - description="A plugin for INGInious to run JPlag module", + description="A plugin for INGInious to run JPlag analyses on student's code.", include_package_data=True, ) diff --git a/static/error.html b/static/error.html deleted file mode 100644 index 0cf231a..0000000 --- a/static/error.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Something goes wrong - - -

Something went wrong while treating your JPlag demand. Contact sysadmins.

- - diff --git a/inginious-jplag/templates/jplag_select_task.html b/templates/jplag_select_task.html similarity index 100% rename from inginious-jplag/templates/jplag_select_task.html rename to templates/jplag_select_task.html diff --git a/templates/jplagresult.html b/templates/jplagresult.html new file mode 100644 index 0000000..e0374c0 --- /dev/null +++ b/templates/jplagresult.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} +{% block title %} JPlag - Analyze of submissions {% endblock %} + +{% block content %} + +

Pay attention that results are only available from inside of UCLouvain network.

+ + + + Download Results + + +{% endblock %} \ No newline at end of file diff --git a/templates/jplagselector.html b/templates/jplagselector.html new file mode 100644 index 0000000..f470530 --- /dev/null +++ b/templates/jplagselector.html @@ -0,0 +1,144 @@ +{% extends "layout.html" %} +{% block title %} JPlag - Analysis of submissions {% endblock %} + +{% block content %} +

Please select submissions to analyse.

+ +
+ + +
+ +
+ +
+ +
    + {% for k, v in subs.items() %} +
  • +
    + {# All checkboxes share the same name; each value is the submission _id. + Flask will return request.form.getlist("submissions") → a plain list of IDs. #} + + +
    +
  • + {% endfor %} +
+
+ + +
+ Code language + +
+ + +
+ File uploads + +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ Options + +
+ + +
+ +
+ + +
+
+ + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/inginious-jplag/templates/test.html b/templates/test.html similarity index 100% rename from inginious-jplag/templates/test.html rename to templates/test.html diff --git a/webpyserv.py b/webpyserv.py deleted file mode 100644 index 8ad812b..0000000 --- a/webpyserv.py +++ /dev/null @@ -1,192 +0,0 @@ -#! /usr/bin/python3 -# -*- coding: utf-8 -*- - -# Copyright (c) 2017 Ludovic Taffin -# This program 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. -# -# This program 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 this program. If not, see . - -## -# Tiny Webpy webserver to treat the data from INGInious and using Jplag executable -# See webpy for more information : http://webpy.org/ -# See Jplag for more information : https://github.com/jplag/jplag -# -## -import datetime -import shutil -import subprocess -import tarfile - -import re -import zipfile - -import yaml -import web -import os -import io - -urls = ( - '/', 'index' -) -t_globals = dict( - datestr=web.datestr, -) -render = web.template.render('templates/', cache=False, - globals=t_globals) -render._keywords['globals']['render'] = render - - -def verify_file_structure(root_submissions_folder,course_id,task_id): - course_folder = root_submissions_folder + course_id - task_folder = course_folder + "/"+task_id - archive_folder = task_folder + "/ARCHIVES" - extraction_task_folder = task_folder + "/EXTRACT" - if not course_id in os.listdir(root_submissions_folder): - os.mkdir(course_folder) - if not task_id in os.listdir(course_folder): - os.mkdir(task_folder) - if not "ARCHIVES" in os.listdir(task_folder): - os.mkdir(archive_folder) - if not "EXTRACT" in os.listdir(task_folder): - os.mkdir(extraction_task_folder) - if not "template" in os.listdir(extraction_task_folder): - os.mkdir(extraction_task_folder + "/template") - - return archive_folder,extraction_task_folder,course_folder,task_folder - - -class index: - def GET(self): - return "Default get response." - - # POST method - # Return the url to the html page of result. - def POST(self): - # Getting data from the form - input_from_web = web.input() - taskId = str(input_from_web["taskId"].decode('UTF-8')) - courseId = str(input_from_web["courseId"].decode('UTF-8')) - language = input_from_web['lang'].decode('UTF-8') - percentage = str(input_from_web['percentage'].decode('UTF-8')) - nyear = str(input_from_web['nyear'].decode('UTF-8')) - check_archives = str(input_from_web['check_archives'].decode('UTF-8')) - submissions = input_from_web["sub.tar"] - exclusion_file = input_from_web["exclude"] - archive_from_previous_year = input_from_web["archi"] - template = input_from_web["template"] - - root_submissions_folder=os.path.dirname(os.path.realpath(__file__)) + "/soumissions/" - archivefolder,extractfolder,course_folder,task_folder = verify_file_structure(root_submissions_folder,courseId,taskId) - - def extract_tar_files_from_bytes(source,desti): - #Extracting submissions to the extract folder - f= open('sub.tar','wb') - f.write(source) - f.close() - file_like_object = io.BytesIO(source) - ar = tarfile.open(fileobj=file_like_object) - ar.extractall(path=desti) - ar.close() - - #Extracting submissions to the extract folder - extract_tar_files_from_bytes(submissions,extractfolder) - #Fill in template folder - if template: - extract_tar_files_from_bytes(template,extractfolder+"/template") - # Getting old and added submissions in the extract - if archive_from_previous_year: - extract_tar_files_from_bytes(archive_from_previous_year,extractfolder) - - #Save exclude file - with open(task_folder+"/exclude.txt", 'wb+') as file: - file.write(exclusion_file) - - - #check if submissions are zip file or code files - for root, subdirs, files in os.walk(extractfolder): - if (root.endswith("uploaded_files")): - for elem in files: - if elem.endswith("zip"): - myzip = zipfile.ZipFile(root + "/" + elem) - try: - for f in myzip.namelist(): - myzip.open(f) - myzip.extract(f, path=root) - except zipfile.BadZipfile: - print("exception") - myzip.close() - elif elem.endswith(".tar.gz") or elem.endswith(".tgz"): - mytar = tarfile.open(root+"/"+elem) - mytar.extractall(path=root) - mytar.close() - else: - pass - - if(check_archives=="on"): - # Compare submission of this year to archived one - for sub in os.listdir(archivefolder): - old_name = archivefolder+"/"+sub - new_name = extractfolder+"/archive_"+sub - try: - os.mkdir(new_name) - shutil.copytree(old_name,new_name) - except FileExistsError: - pass - - ##EXECUTION## - with open("config.yaml", 'r') as stream: - ls = yaml.load(stream, Loader=yaml.CLoader) - loc = ls['jplag']['localisation'] - nameOfJar = ls['jplag']['name'] - completeloc = loc + "/" + nameOfJar - f = open('error.txt', "wb") - - subprocess.call(['java', '-jar', - completeloc, - '-m', percentage + '%', - '-bc', "template/", - '-x', task_folder+"/exclude.txt", - '-l', language, - '-r', os.path.dirname(os.path.realpath(__file__)) + '/static/result/' + courseId + "/" + taskId, - '-s', extractfolder], stdout=f) - f.close() - with open('error.txt', 'r') as f2: - for line in f2.readlines(): - if line.startswith("Error"): - shutil.rmtree(extractfolder, ignore_errors=True) - return web.ctx['homedomain'] + '/static/error.html' - - if(nyear=="on"): - # Store submissions of this submission for future jplag test - its = int(datetime.datetime.now().timestamp()) - for sub in os.listdir(extractfolder): - if not sub.startswith("/archive_"): - new_name = archivefolder+"/"+str(its)+"_"+sub - old_name = extractfolder+"/"+sub - try: - os.mkdir(new_name) - except FileExistsError: - pass - try: - os.rename(old_name,new_name) - except FileNotFoundError: - pass - - shutil.rmtree(extractfolder, ignore_errors=True) - return web.ctx['homedomain'] + '/static/result/' + courseId + "/" + taskId - -app = web.application(urls, globals()) -wsgiapp = app.wsgifunc() - -# Standard running script -if __name__ == "__main__": - app.run()