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.
-
-{% 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.
-
-
-
-
-
-
-
-
-
-
-
-{% 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.
+
+
+
+
+
+
+{% 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()