Skip to content

Commit 384f555

Browse files
committed
Merge branch '10.0.x' of https://github.com/doubtfire-lms/doubtfire-api into 10.0.x
2 parents ca87e2e + 0df6e6f commit 384f555

File tree

28 files changed

+1610
-27
lines changed

28 files changed

+1610
-27
lines changed

.ci-setup/crontab

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ BASH_ENV=/container.env
33
PATH=/tmp/texlive/bin/x86_64-linux:/tmp/texlive/bin/aarch64-linux:/usr/local/bundle/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bundle/bin
44

55
10,15,20,25,30,35,40,45,50,55 * * * * /doubtfire/lib/shell/generate_pdfs.sh
6-
0 5 * * 1,3,5 /doubtfire/lib/shell/check_plagiarism.sh
6+
0 5 * * * /doubtfire/lib/shell/check_plagiarism.sh
77
0 8 * * * /doubtfire/lib/shell/portfolio_autogen_check.sh
88
0 7 * * 1 /doubtfire/lib/shell/send_weekly_emails.sh
99
0 1 * * * /doubtfire/lib/shell/sync_enrolments.sh

.github/workflows/push.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ env:
2929
DF_REDIS_SIDEKIQ_URL: "redis://redis:6379/0"
3030
LATEX_CONTAINER_NAME: doubtfire-texlive
3131
LATEX_BUILD_PATH: /texlive/shell/latex_build.sh
32+
DF_JPLAG_REPORT_DIR: /jplag/results
3233

3334
jobs:
3435
unit-tests:
@@ -61,6 +62,16 @@ jobs:
6162
tags: doubtfire-texlive-development:local
6263
cache-from: type=gha,scope=texlive
6364
cache-to: type=gha,mode=max,scope=texlive
65+
- name: Build JPlag image
66+
uses: docker/build-push-action@v5
67+
with:
68+
context: .
69+
file: jplag.Dockerfile
70+
push: false
71+
load: true
72+
tags: doubtfire-jplag-development:local
73+
cache-from: type=gha,scope=jplag
74+
cache-to: type=gha,mode=max,scope=jplag
6475
- name: Build base doubtfire-api development image
6576
uses: docker/build-push-action@v5
6677
with:
@@ -91,6 +102,26 @@ jobs:
91102
-v ${{ github.workspace }}:/doubtfire
92103
-v /var/run/docker.sock:/var/run/docker.sock
93104
run: docker exec -t ${{ env.LATEX_CONTAINER_NAME }} lualatex -v
105+
- name: Start JPlag service
106+
uses: addnab/docker-run-action@v3
107+
with:
108+
image: doubtfire-jplag-development:local
109+
options: >
110+
--name jplag
111+
-v ${{ github.workspace }}/student-work:/student-work
112+
-v ${{ github.workspace }}/jplag/results:${{ env.DF_JPLAG_REPORT_DIR }}
113+
-v ${{ github.workspace }}/tmp/jplag:/tmp/jplag
114+
--detach
115+
run: sleep infinity
116+
- name: Test JPlag service
117+
uses: addnab/docker-run-action@v3
118+
with:
119+
image: doubtfire-api-development:local
120+
options: >
121+
-t
122+
-v ${{ github.workspace }}:/doubtfire
123+
-v /var/run/docker.sock:/var/run/docker.sock
124+
run: docker exec -t jplag ls /jplag/jplag-jar-with-dependencies.jar
94125
- name: Populate database
95126
uses: addnab/docker-run-action@v3
96127
with:
@@ -118,6 +149,7 @@ jobs:
118149
-e DF_REDIS_SIDEKIQ_URL
119150
-e LATEX_CONTAINER_NAME
120151
-e LATEX_BUILD_PATH
152+
-e DF_JPLAG_REPORT_DIR
121153
run: bundle exec rake db:populate
122154
- name: Run unit tests
123155
uses: addnab/docker-run-action@v3
@@ -127,6 +159,8 @@ jobs:
127159
-v ${{ github.workspace }}:/doubtfire
128160
-v ${{ github.workspace }}/student-work:/student-work
129161
-v /var/run/docker.sock:/var/run/docker.sock
162+
-v ${{ github.workspace }}/jplag/results:${{ env.DF_JPLAG_REPORT_DIR }}
163+
-v ${{ github.workspace }}/tmp/jplag:/tmp/jplag
130164
-e RAILS_ENV
131165
-e DF_STUDENT_WORK_DIR
132166
-e DF_INSTITUTION_HOST
@@ -146,6 +180,9 @@ jobs:
146180
-e DF_REDIS_SIDEKIQ_URL
147181
-e LATEX_CONTAINER_NAME
148182
-e LATEX_BUILD_PATH
183+
-e DF_JPLAG_REPORT_DIR
149184
run: TERM=xterm bundle exec rails test
150185
- name: Stop TexLive service
151186
run: docker rm -f ${{ env.LATEX_CONTAINER_NAME }}
187+
- name: Stop JPlag service
188+
run: docker rm -f jplag

app/api/entities/task_definition_entity.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,12 @@ def staff?(my_role)
4545
expose :scorm_bypass_test
4646
expose :scorm_time_delay_enabled
4747
expose :scorm_attempt_limit
48+
expose :has_jplag_report?, as: :has_jplag_report, if: ->(unit, options) { staff?(options[:my_role]) }
4849
expose :is_graded
4950
expose :max_quality_pts
5051
expose :overseer_image_id, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false
5152
expose :assessment_enabled, if: ->(unit, options) { staff?(options[:my_role]) }
52-
expose :moss_language, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false
53+
expose :similarity_language, if: ->(unit, options) { staff?(options[:my_role]) }, expose_nil: false
5354

5455
expose :learning_outcomes, using: LearningOutcomeEntity, as: :ilos
5556
end

app/api/similarity/entities/task_similarity_entity.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ def staff?(my_role)
1313
similarity.ready_for_viewer?
1414
end
1515

16+
expose :other_task
17+
expose :other_student
18+
1619
expose :parts do |similarity, options|
1720
path = similarity.file_path
1821
has_resource = path.present? && File.exist?(path)
@@ -21,12 +24,13 @@ def staff?(my_role)
2124
{
2225
idx: 0,
2326
format: if has_resource
24-
similarity.type == 'MossTaskSimilarity' ? 'html' : 'pdf'
27+
similarity.type.in?(%w[MossTaskSimilarity JplagTaskSimilarity]) ? 'html' : 'pdf'
2528
end,
26-
description: "#{similarity.student.name} (#{similarity.student.username}) - #{similarity.pct}%"
29+
description: "#{similarity.other_student.name} (#{similarity.other_student.username}) - #{similarity.pct}% similarity"
2730
}
2831
]
2932

33+
# TODO: jplag integration
3034
# For moss similarity, show staff other student details
3135
if similarity.type == 'MossTaskSimilarity' && staff?(options[:my_role])
3236
other_path = similarity.other_similarity&.file_path

app/api/task_definitions_api.rb

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class TaskDefinitionsApi < Grape::API
3232
requires :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed'
3333
optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment'
3434
optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image for overseer'
35-
optional :moss_language, type: String, desc: 'The language to use for code similarity checks'
35+
optional :similarity_language, type: String, desc: 'The language to use for code similarity checks'
3636
optional :scorm_enabled, type: Boolean, desc: 'Whether SCORM assessment is enabled for this task'
3737
optional :scorm_allow_review, type: Boolean, desc: 'Whether a student is allowed to review their completed test attempts'
3838
optional :scorm_bypass_test, type: Boolean, desc: 'Whether a student is allowed to upload files before passing SCORM test'
@@ -69,7 +69,7 @@ class TaskDefinitionsApi < Grape::API
6969
:max_quality_pts,
7070
:assessment_enabled,
7171
:overseer_image_id,
72-
:moss_language,
72+
:similarity_language,
7373
:upload_requirements,
7474
:unit_id
7575
)
@@ -125,7 +125,7 @@ class TaskDefinitionsApi < Grape::API
125125
optional :max_quality_pts, type: Integer, desc: 'A range for quality points when quality is assessed'
126126
optional :assessment_enabled, type: Boolean, desc: 'Enable or disable assessment'
127127
optional :overseer_image_id, type: Integer, desc: 'The id of the Docker image name for overseer'
128-
optional :moss_language, type: String, desc: 'The language to use for code similarity checks'
128+
optional :similarity_language, type: String, desc: 'The language to use for code similarity checks'
129129
end
130130
end
131131
put '/units/:unit_id/task_definitions/:id' do
@@ -158,7 +158,7 @@ class TaskDefinitionsApi < Grape::API
158158
:max_quality_pts,
159159
:assessment_enabled,
160160
:overseer_image_id,
161-
:moss_language,
161+
:similarity_language,
162162
:upload_requirements
163163
)
164164

@@ -711,4 +711,45 @@ class TaskDefinitionsApi < Grape::API
711711
task_def.remove_scorm_data
712712
true
713713
end
714+
715+
desc 'Download the JPLAG report for a given task'
716+
params do
717+
requires :unit_id, type: Integer, desc: 'The unit to download JPLAG report for'
718+
requires :task_def_id, type: Integer, desc: 'The task definition to get the JPLAG report of'
719+
end
720+
get '/units/:unit_id/task_definitions/:task_def_id/jplag_report' do
721+
unit = Unit.find(params[:unit_id])
722+
task_def = unit.task_definitions.find(params[:task_def_id])
723+
unless authorise? current_user, unit, :download_jplag_report
724+
error!({ error: 'Not authorised to download JPLAG reports of unit' }, 403)
725+
end
726+
logger.debug "This is the has_jplag_report? #{task_def.has_jplag_report?}"
727+
if task_def.has_jplag_report?
728+
path = FileHelper.task_jplag_report_path(unit, task_def)
729+
header['Content-Disposition'] = "attachment; filename=#{task_def.abbreviation}-jplag-report.jplag"
730+
else
731+
path = Rails.root.join("public/resources/FileNotFound.pdf")
732+
content_type 'application/pdf'
733+
header['Content-Disposition'] = 'attachment; filename=FileNotFound.pdf'
734+
end
735+
header['Access-Control-Expose-Headers'] = 'Content-Disposition'
736+
content_type 'application/octet-stream'
737+
stream_file path
738+
end
739+
740+
desc 'Get hasJplagReport boolean for a given task'
741+
params do
742+
requires :unit_id, type: Integer, desc: 'The unit to get JPLAG report for'
743+
requires :task_def_id, type: Integer, desc: 'The task definition to get the JPLAG report of'
744+
end
745+
get '/units/:unit_id/task_definitions/:task_def_id/has_jplag_report' do
746+
unit = Unit.find(params[:unit_id])
747+
task_def = unit.task_definitions.find(params[:task_def_id])
748+
749+
unless authorise? current_user, unit, :download_jplag_report
750+
error!({ error: 'Not authorised to download JPLAG reports of unit' }, 403)
751+
end
752+
753+
task_def.has_jplag_report?
754+
end
714755
end

app/helpers/file_helper.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,15 @@ def student_portfolio_path(unit, username, create: true, archived: true)
293293
File.join(student_portfolio_dir(unit, username, create: create, archived: archived), FileHelper.sanitized_filename("#{username}-portfolio.pdf"))
294294
end
295295

296+
def task_jplag_report_dir(unit)
297+
file_server = Doubtfire::Application.config.jplag_report_dir
298+
"#{file_server}/#{unit.code}-#{unit.id}/" # trust the server config and passed in type for paths
299+
end
300+
301+
def task_jplag_report_path(unit, task)
302+
File.join(task_jplag_report_dir(unit), FileHelper.sanitized_filename("#{task.abbreviation}-result.jplag"))
303+
end
304+
296305
def comment_attachment_path(task_comment, attachment_extension)
297306
"#{File.join(student_work_dir(:comment, task_comment.task), "#{task_comment.id.to_s}#{attachment_extension}")}"
298307
end
@@ -783,4 +792,6 @@ def line_wrap(path, width: 160)
783792
module_function :known_extension?
784793
module_function :pages_in_pdf
785794
module_function :line_wrap
795+
module_function :task_jplag_report_dir
796+
module_function :task_jplag_report_path
786797
end
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
class JplagTaskSimilarity < TaskSimilarity
4+
belongs_to :other_task, class_name: 'Task'
5+
6+
def file_path
7+
FileHelper.path_to_plagarism_html(self)
8+
end
9+
10+
#
11+
# Ensure file is also deleted
12+
#
13+
before_destroy do |similarity|
14+
if similarity.task.group_task?
15+
other_tasks = similarity.task.group_submission.tasks.reject { |t| t.id == similarity.task.id }
16+
17+
other_tasks_using_file = other_tasks.select { |t| t.task_similarities.where(other_task_id: similarity.other_task_id).count > 0 }
18+
FileHelper.delete_plagarism_html(similarity) unless other_tasks_using_file.count > 0
19+
else # individual... so can delete file
20+
FileHelper.delete_plagarism_html(similarity)
21+
end
22+
rescue StandardError => e
23+
logger.error "Error deleting match link for task #{similarity.task.id}. Error: #{e.message}"
24+
end
25+
26+
after_destroy do |similarity|
27+
similarity.other_similarity&.destroy
28+
end
29+
30+
def other_similarity
31+
JplagTaskSimilarity.where(task_id: other_task.id, other_task_id: task.id).first unless other_task.nil?
32+
end
33+
34+
def other_student
35+
other_task&.student
36+
end
37+
38+
def other_tutor
39+
other_task&.project&.tutor_for(other_task.task_definition)
40+
end
41+
42+
def other_tutorial
43+
tute = other_task.project.tutorial_for(other_task.task_definition) unless other_task.nil?
44+
tute.nil? ? 'None' : tute.abbreviation
45+
end
46+
47+
def ready_for_viewer?
48+
true
49+
end
50+
end

app/models/similarity/task_definition_similarity_module.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,21 @@ def moss_similarities?
66
MossTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).count > 0
77
end
88

9+
def jplag_similarities?
10+
JplagTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).count > 0
11+
end
12+
913
def clear_related_plagiarism
1014
# delete old plagiarism links
1115
logger.info "Deleting old links for task definition #{id} - #{abbreviation}"
1216
MossTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).find_each do |plnk|
1317
pair = MossTaskSimilarity.find_by(id: plnk.id)
1418
pair.destroy! if pair.present?
1519
end
20+
21+
JplagTaskSimilarity.joins(:task).where('tasks.task_definition_id' => id).find_each do |plnk|
22+
pair = JplagTaskSimilarity.find_by(id: plnk.id)
23+
pair.destroy! if pair.present?
24+
end
1625
end
1726
end

0 commit comments

Comments
 (0)