diff --git a/Gemfile.lock b/Gemfile.lock index 58ca5d592..000d70b11 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -168,6 +168,7 @@ GEM faraday-net_http (3.1.0) net-http ffi (1.17.0-aarch64-linux-gnu) + ffi (1.17.0-x86_64-linux-gnu) fugit (1.11.0) et-orbi (~> 1, >= 1.2.11) raabro (~> 1.4) @@ -265,6 +266,8 @@ GEM nio4r (2.7.3) nokogiri (1.16.5-aarch64-linux) racc (~> 1.4) + nokogiri (1.16.5-x86_64-linux) + racc (~> 1.4) observer (0.1.2) orm_adapter (0.5.0) parallel (1.24.0) @@ -490,6 +493,7 @@ GEM PLATFORMS aarch64-linux + x86_64-linux DEPENDENCIES better_errors diff --git a/app/api/entities/task_definition_entity.rb b/app/api/entities/task_definition_entity.rb index 94ba180d4..edf380b78 100644 --- a/app/api/entities/task_definition_entity.rb +++ b/app/api/entities/task_definition_entity.rb @@ -39,10 +39,11 @@ def staff?(my_role) expose :has_task_sheet?, as: :has_task_sheet expose :has_task_resources?, as: :has_task_resources expose :has_task_assessment_resources?, as: :has_task_assessment_resources, if: ->(unit, options) { staff?(options[:my_role]) } + expose :has_jplag_report?, as: :has_jplag_report, if: ->(unit, options) { staff?(options[:my_role]) } expose :is_graded expose :max_quality_pts expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) } expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) } - expose :moss_language, if: ->(unit, options) { staff?(options[:my_role]) } + expose :jplag_language, if: ->(unit, options) { staff?(options[:my_role]) } end end diff --git a/app/api/similarity/entities/task_similarity_entity.rb b/app/api/similarity/entities/task_similarity_entity.rb index 001f07594..9361246fd 100644 --- a/app/api/similarity/entities/task_similarity_entity.rb +++ b/app/api/similarity/entities/task_similarity_entity.rb @@ -1,9 +1,6 @@ module Similarity module Entities class TaskSimilarityEntity < Grape::Entity - def staff?(my_role) - Role.teaching_staff_ids.include?(my_role.id) unless my_role.nil? - end expose :id expose :type @@ -13,7 +10,7 @@ def staff?(my_role) similarity.ready_for_viewer? end - expose :parts do |similarity, options| + expose :parts do |similarity| path = similarity.file_path has_resource = path.present? && File.exist?(path) @@ -21,24 +18,12 @@ def staff?(my_role) { idx: 0, format: if has_resource - similarity.type == 'MossTaskSimilarity' ? 'html' : 'pdf' + similarity.type == 'JplagTaskSimilarity' ? 'html' : 'pdf' end, - description: "#{similarity.student.name} (#{similarity.student.username}) - #{similarity.pct}%" + description: "#{similarity.other_student.name} (#{similarity.other_student.username}) - #{similarity.pct}% similarity" } ] - # For moss similarity, show staff other student details - if similarity.type == 'MossTaskSimilarity' && staff?(options[:my_role]) - other_path = similarity.other_similarity&.file_path - has_other_resource = other_path.present? && File.exist?(other_path) - - result << { - idx: 1, - format: has_other_resource ? 'html' : nil, - description: "Match: #{similarity.other_student&.name} (#{similarity.other_student&.username}) - #{similarity.other_similarity&.pct}" - } - end - result end end diff --git a/app/api/task_definitions_api.rb b/app/api/task_definitions_api.rb index 03536c9ef..6528489cf 100644 --- a/app/api/task_definitions_api.rb +++ b/app/api/task_definitions_api.rb @@ -32,7 +32,7 @@ class TaskDefinitionsApi < Grape::API requires :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image for overseer' - optional :moss_language, type: String, desc: 'The language to use for code similarity checks' + optional :jplag_language, type: String, desc: 'The language to use for code similarity checks' end end post '/units/:unit_id/task_definitions/' do @@ -61,7 +61,7 @@ class TaskDefinitionsApi < Grape::API :max_quality_pts, :assessment_enabled, :overseer_image_id, - :moss_language + :jplag_language ) task_params[:unit_id] = unit.id @@ -110,7 +110,7 @@ class TaskDefinitionsApi < Grape::API optional :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed' optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment' optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image name for overseer' - optional :moss_language, type: String, desc: 'The language to use for code similarity checks' + optional :jplag_language, type: String, desc: 'The language to use for code similarity checks' end end put '/units/:unit_id/task_definitions/:id' do @@ -138,7 +138,7 @@ class TaskDefinitionsApi < Grape::API :max_quality_pts, :assessment_enabled, :overseer_image_id, - :moss_language + :jplag_language ) task_params[:upload_requirements] = JSON.parse(params[:task_def][:upload_requirements]) unless params[:task_def][:upload_requirements].nil? @@ -614,4 +614,45 @@ class TaskDefinitionsApi < Grape::API stream_file path end + + desc 'Download the JPLAG report for a given task' + params do + requires :unit_id, type: Integer, desc: 'The unit to download JPLAG report for' + requires :task_def_id, type: Integer, desc: 'The task definition to get the JPLAG report of' + end + get '/units/:unit_id/task_definitions/:task_def_id/jplag_report' do + unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) + unless authorise? current_user, unit, :download_jplag_report + error!({ error: 'Not authorised to download JPLAG reports of unit' }, 403) + end + logger.debug "This is the has_jplag_report? #{task_def.has_jplag_report?}" + if task_def.has_jplag_report? + path = FileHelper.task_jplag_report_path(unit, task_def) + header['Content-Disposition'] = "attachment; filename=#{task_def.abbreviation}-jplag-report.zip" + else + path = Rails.root.join('public', 'resources', 'FileNotFound.pdf') + content_type 'application/pdf' + header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf' + end + header['Access-Control-Expose-Headers'] = 'Content-Disposition' + content_type 'application/octet-stream' + stream_file path + end + + desc 'Get hasJplagReport boolean for a given task' + params do + requires :unit_id, type: Integer, desc: 'The unit to get JPLAG report for' + requires :task_def_id, type: Integer, desc: 'The task definition to get the JPLAG report of' + end + get '/units/:unit_id/task_definitions/:task_def_id/has_jplag_report' do + unit = Unit.find(params[:unit_id]) + task_def = unit.task_definitions.find(params[:task_def_id]) + + unless authorise? current_user, unit, :download_jplag_report + error!({ error: 'Not authorised to download JPLAG reports of unit' }, 403) + end + + task_def.has_jplag_report? + end end diff --git a/app/helpers/file_helper.rb b/app/helpers/file_helper.rb index 2ae3597b2..b12f1bf4c 100644 --- a/app/helpers/file_helper.rb +++ b/app/helpers/file_helper.rb @@ -249,6 +249,16 @@ def student_portfolio_path(unit, username, create = true) File.join(student_portfolio_dir(unit, username, create), FileHelper.sanitized_filename("#{username}-portfolio.pdf")) end + def task_jplag_report_dir(unit) + file_server = Doubtfire::Application.config.jplag_report_dir + dst = "#{file_server}/#{unit.code}-#{unit.id}/" # trust the server config and passed in type for paths + dst + end + + def task_jplag_report_path(unit, task) + File.join(task_jplag_report_dir(unit), FileHelper.sanitized_filename("#{task.abbreviation}-result.zip")) + end + def comment_attachment_path(task_comment, attachment_extension) "#{File.join(student_work_dir(:comment, task_comment.task), "#{task_comment.id.to_s}#{attachment_extension}")}" end @@ -658,4 +668,6 @@ def line_wrap(path, width: 160) module_function :known_extension? module_function :pages_in_pdf module_function :line_wrap + module_function :task_jplag_report_dir + module_function :task_jplag_report_path end diff --git a/app/models/similarity/moss_task_similarity.rb b/app/models/similarity/jplag_task_similarity.rb similarity index 89% rename from app/models/similarity/moss_task_similarity.rb rename to app/models/similarity/jplag_task_similarity.rb index bbe066a01..20d5ca1b6 100644 --- a/app/models/similarity/moss_task_similarity.rb +++ b/app/models/similarity/jplag_task_similarity.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class MossTaskSimilarity < TaskSimilarity +class JplagTaskSimilarity < TaskSimilarity belongs_to :other_task, class_name: 'Task' def file_path @@ -28,7 +28,7 @@ def file_path end def other_similarity - MossTaskSimilarity.where(task_id: other_task.id, other_task_id: task.id).first unless other_task.nil? + JplagTaskSimilarity.where(task_id: other_task.id, other_task_id: task.id).first unless other_task.nil? end def other_student diff --git a/app/models/similarity/task_definition_similarity_module.rb b/app/models/similarity/task_definition_similarity_module.rb index 5eb5ed802..491a69790 100644 --- a/app/models/similarity/task_definition_similarity_module.rb +++ b/app/models/similarity/task_definition_similarity_module.rb @@ -3,14 +3,14 @@ # Provides moss and tii similarity features in task definitions module TaskDefinitionSimilarityModule def moss_similarities? - MossTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).count > 0 + JplagTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).count > 0 end def clear_related_plagiarism # delete old plagiarism links logger.info "Deleting old links for task definition #{id} - #{abbreviation}" - MossTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).find_each do |plnk| - pair = MossTaskSimilarity.find_by(id: plnk.id) + JplagTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).find_each do |plnk| + pair = JplagTaskSimilarity.find_by(id: plnk.id) pair.destroy! if pair.present? end end diff --git a/app/models/similarity/unit_similarity_module.rb b/app/models/similarity/unit_similarity_module.rb index c69897f78..302b6c3c6 100644 --- a/app/models/similarity/unit_similarity_module.rb +++ b/app/models/similarity/unit_similarity_module.rb @@ -14,29 +14,42 @@ def last_plagarism_scan end # Pass tasks on to plagarism detection software and setup links between students - def check_moss_similarity(force: false) + def check_similarity(force: false) # Get each task... return unless active # need pwd to restore after cding into submission folder (so the files do not have full path) pwd = FileUtils.pwd + # making temp directory for unit - jplag + root_work_dir = Rails.root.join("tmp", "jplag", "#{code}-#{id}") + unit_code = "#{code}-#{id}" + FileUtils.mkdir_p(root_work_dir) + begin logger.info "Checking plagiarsm for unit #{code} - #{name} (id=#{id})" task_definitions.each do |td| - next if td.moss_language.nil? || td.upload_requirements.nil? || td.upload_requirements.select { |upreq| upreq['type'] == 'code' && upreq['tii_check'] }.empty? + # making temp directory for each task - jplag + tasks_dir = root_work_dir.join(td.id.to_s) + FileUtils.mkdir_p(tasks_dir) - type_data = td.moss_language.split - next if type_data.nil? || (type_data.length != 2) || (type_data[0] != 'moss') + next if td.jplag_language.nil? || td.upload_requirements.nil? || td.upload_requirements.select { |upreq| upreq['type'] == 'code' && upreq['tii_check'] }.empty? # Is there anything to check? logger.debug "Checking plagiarism for #{td.name} (id=#{td.id})" tasks = tasks_for_definition(td) tasks_with_files = tasks.select(&:has_pdf) + run_jplag_on_done_files(td, tasks_dir, tasks_with_files, unit_code) + report_path = "#{Doubtfire::Application.config.jplag_report_dir}/#{unit_code}/#{td.abbreviation}-result.zip" + warn_pct = td.plagiarism_warn_pct || 50 + puts "Warn PCT: #{warn_pct}" + process_jplag_plagiarism_report(report_path, warn_pct, td.group_set) + # Skip if not due yet - next if td.due_date > Time.zone.now + # TODO: Re-enable this after testing + # next if td.due_date > Time.zone.now # Skip if no files changed next unless tasks_with_files.count > 1 && @@ -46,48 +59,7 @@ def check_moss_similarity(force: false) force ) - # There are new tasks, check these - - logger.debug 'Contacting MOSS for new checks' - - # Create the MossRuby object - moss_key = Doubtfire::Application.secrets.secret_key_moss - raise "No moss key set. Check ENV['DF_SECRET_KEY_MOSS'] first." if moss_key.nil? - - moss = MossRuby.new(moss_key) - - # Set options -- the options will already have these default values - moss.options[:max_matches] = 7 - moss.options[:directory_submission] = true - moss.options[:show_num_matches] = 500 - moss.options[:experimental_server] = false - moss.options[:comment] = '' - moss.options[:language] = type_data[1] - - tmp_path = File.join(Dir.tmpdir, 'doubtfire', "check-#{id}-#{td.id}") - - begin - # Create a file hash, with the files to be processed - to_check = MossRuby.empty_file_hash - add_done_files_for_plagiarism_check_of(td, tmp_path, to_check, tasks_with_files) - - FileUtils.chdir(tmp_path) - - # Get server to process files - logger.debug 'Sending to MOSS...' - url = moss.check(to_check, ->(_) { print '.' }) - - logger.info "MOSS check for #{code} #{td.abbreviation} url: #{url}" - - td.plagiarism_report_url = url - td.plagiarism_updated = true - td.save - rescue StandardError => e - logger.error "Failed to check plagiarism for task #{td.name} (id=#{td.id}). Error: #{e.message}" - ensure - FileUtils.chdir(pwd) - FileUtils.rm_rf tmp_path - end + # There are new tasks, check these with JPLAG end self.last_plagarism_scan = Time.zone.now save! @@ -98,128 +70,156 @@ def check_moss_similarity(force: false) self end - def update_plagiarism_stats - moss_key = Doubtfire::Application.secrets.secret_key_moss - raise "No moss key set. Check ENV['DF_SECRET_KEY_MOSS'] first." if moss_key.nil? + private - moss = MossRuby.new(moss_key) + # Extract all done files related to a task definition matching a pattern into a given directory. + # Returns an array of files + # def add_done_files_for_plagiarism_check_of(task_definition, tmp_path, tasks_with_files) + # # get each code file for each task + # task_definition.upload_requirements.each_with_index do |upreq, idx| + # # only check code files marked for similarity checks + # next unless upreq['type'] == 'code' && upreq['tii_check'] +# + # pattern = task_definition.glob_for_upload_requirement(idx) +# + # tasks_with_files.each do |t| + # t.extract_file_from_done(tmp_path, pattern, ->(_task, to_path, name) { File.join(to_path.to_s, t.student.username.to_s, name.to_s) }) + # end + # end +# + # self + # end + + # JPLAG Function - extracts "done" files for each task and packages them into a directory for JPLAG to run on + def run_jplag_on_done_files(task_definition, tasks_dir, tasks_with_files, unit_code) + similarity_pct = task_definition.plagiarism_warn_pct + return if similarity_pct.nil? + + # Check if the directory exists and create it if it doesn't + results_dir = "/jplag/results/#{unit_code}" + `docker exec jplag sh -c 'if [ ! -d "#{results_dir}" ]; then mkdir -p "#{results_dir}"; fi'` + + # Remove existing result file if it exists + result_file = "#{results_dir}/#{task_definition.abbreviation}-result.zip" + `docker exec jplag sh -c 'if [ -f "#{result_file}" ]; then rm "#{result_file}"; fi'` - task_definitions.where(plagiarism_updated: true).find_each do |td| - td.plagiarism_updated = false - td.save + # get each code file for each task + task_definition.upload_requirements.each_with_index do |upreq, idx| + # only check code files marked for similarity checks + next unless upreq['type'] == 'code' && upreq['tii_check'] - # Get results - url = td.plagiarism_report_url - logger.debug "Processing MOSS results #{url}" + pattern = task_definition.glob_for_upload_requirement(idx) - warn_pct = td.plagiarism_warn_pct || 50 + tasks_with_files.each do |t| + t.extract_file_from_done(tasks_dir, pattern, ->(_task, to_path, name) { File.join(to_path.to_s, t.student.username.to_s, name.to_s) }) + end - results = moss.extract_results(url, warn_pct, ->(line) { puts line }) + logger.info "Starting JPLAG container to run on #{tasks_dir}" + root_dir = Rails.root.to_s + tasks_dir_split = tasks_dir.to_s.split(root_dir)[1] + file_lang = task_definition.jplag_language.to_s - # Use results - results.each do |match| - task_id1 = %r{.*/(\d+)/$}.match(match[0][:filename])[1] - task_id2 = %r{.*/(\d+)/$}.match(match[1][:filename])[1] + # Run JPLAG on the extracted files + `docker exec jplag java -jar /jplag/myJplag.jar #{tasks_dir_split} -l #{file_lang} --similarity-threshold=#{similarity_pct} -M RUN -r #{results_dir}/#{task_definition.abbreviation}-result` + end - t1 = Task.find(task_id1) - t2 = Task.find(task_id2) + # Delete the extracted code files from tmp + tmp_dir = Rails.root.join("tmp", "jplag") + logger.info "Deleting files in: #{tmp_dir}" + logger.info "Files to delete: #{Dir.glob("#{tmp_dir}/*")}" + FileUtils.rm_rf(Dir.glob("#{tmp_dir}/*")) + self + end - if t1.nil? || t2.nil? - logger.error "Could not find tasks #{task_id1} or #{task_id2} for plagiarism stats check!" - next + def process_jplag_plagiarism_report(path, warn_pct, is_group) + # Extract overview json from report zip + Zip::File.open(path) do |zip_file| + overview_entry = zip_file.find_entry('overview.json') + + if overview_entry + # Read the contents of overview.json + overview_content = overview_entry.get_input_stream.read + + # Parse the JSON into a Ruby hash + overview_data = JSON.parse(overview_content) + + # Iterate over the "top_comparisons" array and collect the required fields + top_comparisons = overview_data['top_comparisons'].map do |comparison| + { + first_submission: comparison['first_submission'], + second_submission: comparison['second_submission'], + max_similarity: comparison['similarities']['MAX'] * 100 + } end - if td.group_set # its a group task - g1_tasks = t1.group_submission.tasks - g2_tasks = t2.group_submission.tasks - - g1_tasks.each do |gt1| - g2_tasks.each do |gt2| - create_plagiarism_link(gt1, gt2, match, warn_pct) + # Save the results to the database + top_comparisons.each do |comparison| + task1_id = nil + task2_id = nil + zip_file.each do |entry| + if entry.name =~ %r{\Afiles/#{comparison[:first_submission]}/} + task1_id = entry.name.split('/')[2].to_i + elsif entry.name =~ %r{\Afiles/#{comparison[:second_submission]}/} + task2_id = entry.name.split('/')[2].to_i end end + first_submission = Task.find(task1_id) if task1_id + second_submission = Task.find(task2_id) if task2_id + + if first_submission.nil? || second_submission.nil? + logger.error "Could not find tasks #{comparison[:first_submission]} or #{comparison[:second_submission]} for plagiarism stats check!" + next + end - else # just link the individuals... - create_plagiarism_link(t1, t2, match, warn_pct) + if is_group # its a group task + g1_tasks = first_submission.group_submission.tasks + g2_tasks = second_submission.group_submission.tasks + g1_tasks.each do |gt1| + g2_tasks.each do |gt2| + next if gt1.student == gt2.student + create_plagiarism_link(gt1, gt2, warn_pct, comparison[:max_similarity]) + end + end + else # just link the individuals... + create_plagiarism_link(first_submission, second_submission, warn_pct, comparison[:max_similarity]) + end end + else + puts 'overview.json not found in the zip file' end - end - - self.last_plagarism_scan = Time.zone.now - save! - self + self + end end - private - - def create_plagiarism_link(task1, task2, match, warn_pct) - plk1 = MossTaskSimilarity.where(task_id: task1.id, other_task_id: task2.id).first - plk2 = MossTaskSimilarity.where(task_id: task2.id, other_task_id: task1.id).first - + def create_plagiarism_link(task1, task2, warn_pct, max_similarity) + # Create a new plagiarism link between the two tasks + plk1 = JplagTaskSimilarity.where(task_id: task1.id, other_task_id: task2.id).first + plk2 = JplagTaskSimilarity.where(task_id: task2.id, other_task_id: task1.id).first if plk1.nil? || plk2.nil? # Delete old links between tasks plk1&.destroy ## will delete its pair plk2&.destroy - - plk1 = MossTaskSimilarity.create do |plm| + plk1 = JplagTaskSimilarity.create do |plm| plm.task = task1 plm.other_task = task2 - plm.pct = match[0][:pct] + plm.pct = max_similarity plm.flagged = plm.pct >= warn_pct end - - plk2 = MossTaskSimilarity.create do |plm| + plk2 = JplagTaskSimilarity.create do |plm| plm.task = task2 plm.other_task = task1 - plm.pct = match[1][:pct] + plm.pct = max_similarity plm.flagged = plm.pct >= warn_pct end else - # puts "#{plk1.pct} != #{match[0][:pct]}, #{plk1.pct != match[0][:pct]}" - # Flag is larger than warn pct and larger than previous pct - plk1.flagged = match[0][:pct] >= warn_pct && match[0][:pct] >= plk1.pct - plk2.flagged = match[1][:pct] >= warn_pct && match[1][:pct] >= plk2.pct - - plk1.pct = match[0][:pct] - plk2.pct = match[1][:pct] + plk1.flagged = max_similarity >= warn_pct && max_similarity >= plk1.pct + plk2.flagged = max_similarity >= warn_pct && max_similarity >= plk2.pct + plk1.pct = max_similarity + plk2.pct = max_similarity end - - plk1.plagiarism_report_url = match[0][:url] - plk2.plagiarism_report_url = match[1][:url] - plk1.save! plk2.save! - - FileHelper.save_plagiarism_html(plk1, match[0][:html]) - FileHelper.save_plagiarism_html(plk2, match[1][:html]) - end - - # - # Extract all done files related to a task definition matching a pattern into a given directory. - # Returns an array of files - # - def add_done_files_for_plagiarism_check_of(task_definition, tmp_path, to_check, tasks_with_files) - type_data = task_definition.moss_language.split - return if type_data.nil? || (type_data.length != 2) || (type_data[0] != 'moss') - - # get each code file for each task - task_definition.upload_requirements.each_with_index do |upreq, idx| - # only check code files marked for similarity checks - next unless upreq['type'] == 'code' && upreq['tii_check'] - - pattern = task_definition.glob_for_upload_requirement(idx) - - tasks_with_files.each do |t| - t.extract_file_from_done(tmp_path, pattern, ->(_task, to_path, name) { File.join(to_path.to_s, t.student.username.to_s, name.to_s) }) - end - - # extract files matching each pattern - # -- each pattern - MossRuby.add_file(to_check, "**/#{pattern}") - end - - self end end diff --git a/app/models/task.rb b/app/models/task.rb index e75815f90..1fe630f9a 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -116,7 +116,7 @@ def specific_permission_hash(role, perm_hash, _other) has_many :comments, class_name: 'TaskComment', dependent: :destroy, inverse_of: :task has_many :task_similarities, class_name: 'TaskSimilarity', dependent: :destroy, inverse_of: :task - has_many :reverse_task_similarities, class_name: 'MossTaskSimilarity', dependent: :destroy, inverse_of: :other_task, foreign_key: 'other_task_id' + has_many :reverse_task_similarities, class_name: 'JplagTaskSimilarity', dependent: :destroy, inverse_of: :other_task, foreign_key: 'other_task_id' has_many :learning_outcome_task_links, dependent: :destroy # links to learning outcomes has_many :learning_outcomes, through: :learning_outcome_task_links has_many :task_engagements, dependent: :destroy diff --git a/app/models/task_definition.rb b/app/models/task_definition.rb index 7e78bd2a7..18fded61d 100644 --- a/app/models/task_definition.rb +++ b/app/models/task_definition.rb @@ -384,6 +384,10 @@ def has_task_sheet? File.exist? task_sheet end + def has_jplag_report? + File.exist? jplag_report + end + def is_graded? is_graded end @@ -449,6 +453,10 @@ def task_assessment_resources task_assessment_resources_with_abbreviation(abbreviation) end + def jplag_report + task_jplag_report_with_abbreviation(abbreviation) + end + def related_tasks_with_files(consolidate_groups = true) tasks_with_files = tasks.select(&:has_pdf) @@ -537,4 +545,17 @@ def task_assessment_resources_with_abbreviation(abbr) result_with_sanitised_file end end + + def task_jplag_report_with_abbreviation(abbr) + task_path = FileHelper.task_jplag_report_dir unit + + result_with_sanitised_path = "#{task_path}#{FileHelper.sanitized_path(abbr)}-result.zip" + result_with_sanitised_file = "#{task_path}#{FileHelper.sanitized_filename(abbr)}-result.zip" + + if File.exist? result_with_sanitised_path + result_with_sanitised_path + else + result_with_sanitised_file + end + end end diff --git a/app/models/unit.rb b/app/models/unit.rb index 175e62c79..d53a0932f 100644 --- a/app/models/unit.rb +++ b/app/models/unit.rb @@ -28,6 +28,7 @@ def self.permissions :download_stats, :download_unit_csv, :download_grades, + :download_jplag_report, :exceed_capacity ] @@ -46,6 +47,7 @@ def self.permissions :change_project_enrolment, :download_stats, :download_grades, + :download_jplag_report, :rollover_unit, :exceed_capacity, :perform_overseer_assessment_test @@ -66,6 +68,7 @@ def self.permissions :download_stats, :download_unit_csv, :download_grades, + :download_jplag_report, :exceed_capacity ] @@ -177,6 +180,7 @@ def role_for(user) scope :set_inactive, -> { where('active = ?', false) } include UnitTiiModule + include UnitSimilarityModule def detailed_name diff --git a/config/application.rb b/config/application.rb index df21df01b..0149682c3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,6 +31,12 @@ class Application < Rails::Application # variable. config.student_work_dir = ENV['DF_STUDENT_WORK_DIR'] || "#{Rails.root}/student_work" + # ==> JPLAG report directory + # File server location for storing JPLAG reports. Defaults to `jplag_results` + # directory under root but is overridden using DF_JPLAG_REPORT_DIR environment + # variable. + config.jplag_report_dir = ENV['DF_JPLAG_REPORT_DIR'] || "#{Rails.root}/jplag_results" + # ==> Load credentials from env credentials.secret_key_base = ENV.fetch('DF_SECRET_KEY_BASE', Rails.env.production? ? nil : '9e010ee2f52af762916406fd2ac488c5694a6cc784777136e657511f8bbc7a73f96d59c0a9a778a0d7cf6406f8ecbf77efe4701dfbd63d8248fc7cc7f32dea97') credentials.secret_key_attr = ENV.fetch('DF_SECRET_KEY_ATTR', Rails.env.production? ? nil : 'e69fc5960ca0e8700844a3a25fe80373b41c0a265d342eba06950113f3766fd983bad9ec51bf36eb615d9711bfe1dd90b8e35f01841b323f604ffee857e32055') diff --git a/db/migrate/20240105055902_add_tii_details.rb b/db/migrate/20240105055902_add_tii_details.rb index a571f80d1..745d30ce4 100644 --- a/db/migrate/20240105055902_add_tii_details.rb +++ b/db/migrate/20240105055902_add_tii_details.rb @@ -7,7 +7,7 @@ def change add_column :units, :tii_group_context_id, :string add_column :task_definitions, :tii_group_id, :string - add_column :task_definitions, :moss_language, :string + add_column :task_definitions, :jplag_language, :string rename_table :plagiarism_match_links, :task_similarities @@ -88,7 +88,7 @@ def change next unless plagiarism_checks.any? - task_definition.update(moss_language: plagiarism_checks.first['type']) + task_definition.update(jplag_language: plagiarism_checks.first['type']) task_definition.upload_requirements.each do |upload_requirement| next unless upload_requirement['type'] == 'code' upload_requirement['tii_check'] = true diff --git a/db/schema.rb b/db/schema.rb index 6daa71ebf..65679b636 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -249,7 +249,7 @@ t.boolean "assessment_enabled", default: false t.bigint "overseer_image_id" t.string "tii_group_id" - t.string "moss_language" + t.string "jplag_language" t.index ["group_set_id"], name: "index_task_definitions_on_group_set_id" t.index ["overseer_image_id"], name: "index_task_definitions_on_overseer_image_id" t.index ["tutorial_stream_id"], name: "index_task_definitions_on_tutorial_stream_id" diff --git a/lib/tasks/checks.rake b/lib/tasks/checks.rake index 812a42ecf..18d783e4f 100644 --- a/lib/tasks/checks.rake +++ b/lib/tasks/checks.rake @@ -69,7 +69,7 @@ namespace :submission do puts ' ------------------------------------------------------------ ' puts " Starting Plagiarism Check for #{unit.name}" puts ' ------------------------------------------------------------ ' - unit.check_moss_similarity + unit.check_similarity end puts ' ------------------------------------------------------------ ' puts ' done.'