-
Notifications
You must be signed in to change notification settings - Fork 0
Run a JPLAG analysis as a container job #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
AlexandreDoneux
wants to merge
16
commits into
main
Choose a base branch
from
run_as_job
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
a8fa9b2
Removing webserver part of the plugin
AlexandreDoneux 718dc69
Moving __init.py__ and templates directory to root
AlexandreDoneux b442405
Adapt setuptools config files
AlexandreDoneux 0d110c9
[container] Adding JPLAG environment from PR previously in main repos…
AlexandreDoneux ff0ce51
Functionnal JPLAG analysis run as job
AlexandreDoneux 3e92d37
[container] Checking for long enough submissions
AlexandreDoneux 6181961
Give correct code extension to file in submissions
AlexandreDoneux e209154
[container] Change result folder to archive folder
AlexandreDoneux 14c2553
Adding gitignore
AlexandreDoneux 3d96643
Rolling back to previous code
AlexandreDoneux cb923b6
Adding Result download
AlexandreDoneux 79fe15a
[container] Refactoring run file, cleaning it
AlexandreDoneux 3cea52c
Removing save of the submission for a future year
AlexandreDoneux 845907d
Remove old get_submission_archive() method + rename additional submis…
AlexandreDoneux fbdcc29
Take into account evaluation mode of a task when fetching the submiss…
AlexandreDoneux d681d5e
Add course id, tak id and date to downloaded result
AlexandreDoneux File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| recursive-include inginious-jplag/templates/ * | ||
| recursive-include templates/ * |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,250 @@ | ||
| # -*- 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 <http://www.gnu.org/licenses/>. | ||
|
|
||
| # JPLAG plugin for INGInious | ||
| import os | ||
| import io | ||
| import logging | ||
| import requests | ||
| import tarfile, tempfile | ||
| import base64 | ||
| import zipfile | ||
|
|
||
| from flask import request, render_template | ||
| from werkzeug.exceptions import NotFound | ||
|
|
||
| from inginious.frontend.pages.course_admin.utils import INGIniousAdminPage | ||
| from inginious.common.base import load_json_or_yaml | ||
| 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', '<i class="fa fa-search fa-fw"></i> JPlag') | ||
|
|
||
| def get_submission_archive_here(submissions, archive_file): | ||
| file_to_put = {} | ||
|
|
||
| # Build unique base paths per submission | ||
| 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 | ||
|
|
||
| # Create the final tar.gz | ||
| with tarfile.open(fileobj=archive_file, mode="w:gz") as tar: | ||
|
|
||
| for base_path, submission in file_to_put.items(): | ||
|
|
||
| # If the submission has an archive attached | ||
| if submission.archive: | ||
| # Read FileField contents into memory | ||
| archive_bytes = submission.archive.read() | ||
| archive_buffer = io.BytesIO(archive_bytes) | ||
|
|
||
| # Open the submission's archive | ||
| with tarfile.open(fileobj=archive_buffer, mode="r:*") as subtar: | ||
| for member in subtar.getmembers(): | ||
| # Skip directories without content | ||
| subfile = subtar.extractfile(member) | ||
| if subfile is None: | ||
| continue | ||
|
|
||
| # Rewrite the path inside the combined archive | ||
| member.name = f"{base_path}/archive/{member.name}" | ||
|
|
||
| tar.addfile(member, subfile) | ||
|
|
||
| # Reset cursor so caller can read it | ||
| archive_file.seek(0) | ||
| return archive_file | ||
|
|
||
|
|
||
| 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", | ||
| # Add multilanguage ? | ||
| # remove some alpha languages ? | ||
| } | ||
|
|
||
|
|
||
| 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] | ||
| res = {elem['username'][0]: elem for elem in subs} | ||
|
|
||
| # res -> dernière soumission par username ? | ||
| # qu'en est-il de si on garde la meilleur solution et pas la dernière ? | ||
|
|
||
| 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"} | ||
|
|
||
| # get all info from the form | ||
|
|
||
| selected_submission_ids = request.form.getlist("submissions") | ||
| language = request.form.get("language", "java17") # TODO : modify default to 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 | ||
|
|
||
| # TODO : better value check | ||
| save_for_next_year = request.form.get("nextyear", "0") == "1" | ||
| compare_with_archives = request.form.get("addarchives", "0") == "1" | ||
|
|
||
| # TODO : handle files (template, old submissions, exclude) | ||
| # pass them as input to the container and them create them in the container at the start of the runfile ? | ||
|
|
||
|
|
||
|
|
||
| # TODO : create a zip file byte string made from all submissions archives (with info about the students) | ||
|
|
||
| # 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) | ||
| task = course.get_task(taskid) | ||
|
|
||
| # create a temporary directory to store the submissions and their inputs | ||
| with tempfile.TemporaryDirectory() as temp_dir: | ||
|
|
||
| # for each submission, | ||
| for sub in subs: | ||
| # get inputs and save them into appropriate files | ||
| input = self.submission_manager.get_input_from_submission(sub, only_input=True) | ||
| for problem in task.get_problems(): | ||
| # create a file with the name of the problem and load the input into it | ||
| problem_id = problem.get_id() | ||
| problem_input = input.get(problem_id, None) | ||
| if problem_input is not None: | ||
| problem_file_path = os.path.join(temp_dir, f"{sub['username'][0]}/{problem_id}.txt") | ||
| # create the directory/file if it doesn't exist | ||
| os.makedirs(os.path.dirname(problem_file_path), exist_ok=True) | ||
| # TODO : rename after used programming language extension (e.g. .py, .java, .cpp, etc.) -> problem["language"] | ||
| # TODO : handle submissions for multiple users (groups) | ||
| with open(problem_file_path, "w") as f: | ||
| f.write(problem_input) | ||
|
|
||
| # TODO : handle files given by student, not only the input | ||
| # get the uploaded_files folder and put it in the apropriate place | ||
|
|
||
| # zip directory | ||
|
|
||
| # Zip the temp_dir into an in-memory buffer | ||
| zip_buffer = io.BytesIO() | ||
| with zipfile.ZipFile(zip_buffer, "w", compression=zipfile.ZIP_DEFLATED) as zf: | ||
| for dirpath, dirnames, filenames in os.walk(temp_dir): | ||
| for filename in filenames: | ||
| full_path = os.path.join(dirpath, filename) | ||
| # Store relative path inside the zip (strips the temp_dir prefix) | ||
| arcname = os.path.relpath(full_path, temp_dir) | ||
| zf.write(full_path, arcname) | ||
|
|
||
| zip_bytes = zip_buffer.getvalue() | ||
|
|
||
|
|
||
| job_input = { | ||
| 'submissions': { | ||
| 'filename': 'submissions.zip', | ||
| 'value': zip_bytes , | ||
| }, | ||
| 'language': language, | ||
| 'similarity_threshold': percentage, | ||
| } | ||
| # change variable name in job_input ? | ||
|
|
||
| # TODO : detect file types to analyze, run a JPLAG analysis per type (in one job) | ||
|
|
||
|
|
||
|
|
||
| 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) | ||
|
|
||
|
|
||
|
|
||
|
|
||
| # temporary render. Page will need to be made better | ||
| return render_template("jplag/templates/jplagresult.html", template_folder=PATH_TO_TEMPLATES,url="") | ||
|
|
||
| plugin_manager.add_page("/jplag/<courseid>/<taskid>", JPLAGPage.as_view("jplagpage")) | ||
| plugin_manager.add_page("/jplagselecttask/<courseid>/", JPLAGSelectPage.as_view("jplagselectpage")) | ||
| plugin_manager.add_hook('course_admin_menu', add_admin_menu) | ||
| plugin_manager.add_template_prefix("jplag", PATH_TO_PLUGIN) | ||
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.