Skip to content

Commit

Permalink
Merge pull request #5534 from dodona-edu/feat/evaluation-plagiarism
Browse files Browse the repository at this point in the history
Add plagiarism detection for evaluations
  • Loading branch information
jorg-vr committed May 28, 2024
2 parents b92e8f6 + 29be82b commit b215875
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 39 deletions.
11 changes: 7 additions & 4 deletions app/assets/javascripts/dolos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import { html, render } from "lit";
import { i18n } from "i18n/i18n";

const LOADER_ID = "dolos-loader";
const BTN_ID = "dolos-btn";
const DOLOS_URL = "/dolos_reports";

export async function startDolos(url: string): Promise<void> {
export function initDolosBtn(btnID: string, url: string): void {
const btn = document.getElementById(btnID) as HTMLLinkElement;
btn.addEventListener("click", () => startDolos(btn, url));
}

export async function startDolos(btn: HTMLLinkElement, url: string): Promise<void> {
const loader = document.getElementById(LOADER_ID) as LoadingBar;
loader.show();
const btn = document.getElementById(BTN_ID) as HTMLLinkElement;
btn.classList.add("disabled");

const settings = new FormData();
Expand Down Expand Up @@ -43,7 +46,7 @@ export async function startDolos(url: string): Promise<void> {
loader.hide();

const newBtn = html`
<a id="${BTN_ID}" class="btn btn-outline with-icon" href="${dolosUrl}" target="_blank">
<a id="${btn.id}" class="btn btn-outline with-icon" href="${dolosUrl}" target="_blank">
<i class="mdi mdi-graph-outline mdi-18"></i> ${i18n.t("js.dolos.view_report")}
</a>
`;
Expand Down
8 changes: 3 additions & 5 deletions app/assets/stylesheets/components/table.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,9 @@
.table-resource .actions {
text-align: right;

&.submissions-table {
// by default the size of the element in the col is used as width if width is smaller than that element.
// This width is just here to force everything to the right
width: 1px;
}
// by default the size of the element in the col is used as width if width is smaller than that element.
// This width is just here to force everything to the right
width: 1px;
}

tr.gu-mirror {
Expand Down
11 changes: 9 additions & 2 deletions app/helpers/export_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ class Zipper

attr_reader :users, :item, :errors

CONVERT_TO_BOOL = %w[deadline only_last_submission with_info all with_labels].freeze
SUPPORTED_OPTIONS = %w[deadline filter_students group_by only_last_submission with_info all with_labels].freeze
CONVERT_TO_BOOL = %w[deadline only_last_submission with_info all with_labels evaluation].freeze
SUPPORTED_OPTIONS = %w[deadline filter_students group_by only_last_submission with_info all with_labels evaluation].freeze

# Keywords used:
# :item : A User, Course or Series for which submissions will be exported
Expand All @@ -32,6 +32,7 @@ def initialize(**kwargs)
.to_h { |m| [m.user, m.course_labels] }

@users = @users_labels.keys if users.nil?
@users = @item.evaluation.users if evaluation?
when Course
@list = @item.series if all?
@users_labels = @item.course_memberships
Expand Down Expand Up @@ -92,6 +93,10 @@ def all?
@options[:all].present?
end

def evaluation?
@options[:evaluation].present? && @item.is_a?(Series) && @item.evaluation.present?
end

def zip_filename
name_source = @list.present? && @list.one? ? @list.first : @item
base_name = name_source.is_a?(User) ? name_source.full_name : name_source.name
Expand Down Expand Up @@ -242,6 +247,8 @@ def bundle
end

def get_submissions_for_series(series, selected_exercises, users)
return policy_scope(series.evaluation.submissions.where(user_id: users.map(&:id), exercise_id: selected_exercises.map(&:id))) if evaluation?

submissions = policy_scope(Submission).where(user_id: users.map(&:id), exercise_id: selected_exercises.map(&:id), course: series.course_id).includes(:user, :exercise)
submissions = submissions.before_deadline(@options[:deadline]) if deadline?
submissions = submissions.group(:user_id, :exercise_id).most_recent if only_last_submission?
Expand Down
4 changes: 2 additions & 2 deletions app/javascript/packs/dolos.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { startDolos } from "dolos.ts";
import { initDolosBtn } from "dolos.ts";

window.dodona.startDolos = startDolos;
window.dodona.initDolosBtn = initDolosBtn;
1 change: 1 addition & 0 deletions app/models/evaluation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Evaluation < ApplicationRecord
has_many :users, through: :evaluation_users
has_many :exercises, through: :evaluation_exercises
has_many :score_items, through: :evaluation_exercises
has_many :submissions, through: :feedbacks

has_many :annotated_submissions, -> { distinct }, through: :annotations, source: :submission

Expand Down
27 changes: 22 additions & 5 deletions app/views/evaluations/_exercises_progress_table.html.erb
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<%= content_for :javascripts do %>
<%= javascript_include_tag 'dolos' %>
<% end %>
<div class="table-scroll-wrapper">
<d-loading-bar id="dolos-loader"></d-loading-bar>
<table class="table activity-table table-resource">
<thead>
<tr>
<th class="status-icon"></th>
<th><%= t "activities.index.activity_title" %></th>
<th class='count'><%= t "evaluations.show.evaluation_progress" %></th>
<th class="status-icon"></th>
<th class="actions"></th>
</tr>
</thead>
<tbody>
Expand All @@ -28,14 +32,27 @@
</span>
</td>

<td>
<td class="actions">
<% if meta[:next_incomplete_feedback].present? %>
<%= link_to meta[:next_incomplete_feedback], title: t('evaluations.show.next_incomplete_feedback'), class: 'btn btn-icon' do %>
<i class="mdi mdi-chevron-right"></i>
<%= link_to meta[:next_incomplete_feedback], title: t('evaluations.show.next_incomplete_feedback'), class: 'btn btn-outline with-icon' do %>
<i class="mdi mdi-message-draw mdi-18"></i>
<%= t(".evaluate") %>
<% end %>
<% else %>
<span><i class="mdi mdi-check colored-correct"></i></span>
<a class="btn btn-outline disabled with-icon">
<i class="mdi mdi-check mdi-18"></i>
<%= t(".evaluated") %>
</a>
<% end %>
<a class="btn btn-outline with-icon" id="dolos-btn-<%= meta[:exercise].id %>">
<i class="mdi mdi-graph-outline mdi-18"></i>
<%= t "submissions.index.detect_plagiarism" %>
</a>
<script>
dodona.ready.then(() =>{
dodona.initDolosBtn('dolos-btn-<%= meta[:exercise].id %>', "<%= series_exports_path(@evaluation.series, token: (@evaluation.series.access_token if @evaluation.series.hidden?), selected_ids: [meta[:exercise].id], evaluation: true) %>");
})
</script>
</td>
</tr>
<% end %>
Expand Down
26 changes: 11 additions & 15 deletions app/views/evaluations/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,17 @@
<h2 class="card-title-text"><%= t('.title', series: @evaluation.series.name) %></h2>
</div>
<div class="card-supporting-text">
<div class="row">
<div class="col-lg-6 col-md-12 order-lg-1">
<div class="callout callout-info">
<h4><%= t ".explanation_title" %></h4>
<% if @evaluation.score_items.empty? %>
<%= t ".explanation_no_grading_html" %>
<% else %>
<%= t ".explanation_yes_grading_html" %>
<% end %>
</div>
</div>
<div class="col-lg-6 col-md-12 order-lg-0">
<p><%= t '.deadline_html', users: @evaluation.users.count, exercises: @evaluation.exercises.count, deadline: l(@evaluation.deadline, format: :submission) %></p>
<%= render partial: 'exercises_progress_table', locals: { metadata: @evaluation.metadata, series: @evaluation.series } %>
</div>
<details>
<summary><%= t ".explanation_title" %></summary>
<% if @evaluation.score_items.empty? %>
<%= t ".explanation_no_grading_html" %>
<% else %>
<%= t ".explanation_yes_grading_html" %>
<% end %>
</details>
<div>
<p><%= t '.deadline_html', users: @evaluation.users.count, exercises: @evaluation.exercises.count, deadline: l(@evaluation.deadline, format: :submission) %></p>
<%= render partial: 'exercises_progress_table', locals: { metadata: @evaluation.metadata, series: @evaluation.series } %>
</div>
</div>
<div class="card-actions card-border">
Expand Down
13 changes: 11 additions & 2 deletions app/views/submissions/index.html.erb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<%= render 'activities/navbar_links' if @activity %>
<%= render 'courses/navbar_links' if @course && !@activity %>
<%= javascript_include_tag 'dolos' %>
<%= content_for :javascripts do %>
<%= javascript_include_tag 'dolos' %>
<% end %>
<div class="row">
<div class="col-12">
<div class="card">
Expand Down Expand Up @@ -43,10 +45,17 @@
<% if current_user&.course_admin?(@course) %>
<%
actions << {icon: 'replay', text: t(".reevaluate_submissions"), confirm: t(".confirm_reevaluate_submissions"), action: mass_rejudge_submissions_path(user_id: @user&.id, activity_id: @activity&.id, course_id: @course&.id, series_id: @series&.id, judge_id: @judge&.id)} if policy(Submission).mass_rejudge?
actions << {icon: 'graph-outline', text: t('.detect_plagiarism'), js: "window.dodona.startDolos(\"#{series_exports_path(@series, token: (@series.access_token if @series.hidden?), selected_ids: [@activity.id])}\")", id: "dolos-btn" } if @series && @activity
options << {label: t('.most_recent'), param: 'most_recent_per_user'} if @activity
options << {label: t('.watch_submissions'), param: 'refresh'}
%>
<% if @series.present? %>
<% actions << {icon: 'graph-outline', text: t('.detect_plagiarism'), id: "dolos-btn" } if @series && @activity %>
<script>
dodona.ready.then(() => {
dodona.initDolosBtn("dolos-btn", "<%= series_exports_path(@series, token: (@series.access_token if @series.hidden?), selected_ids: [@activity.id]) %>");
});
</script>
<% end %>
<% end %>
<%= render partial: 'layouts/searchbar', locals: {actions: actions, options: options, course_labels: @course_labels, statuses: Submission.statuses.keys, refresh_element: "#refresh_element"} %>
<div id="submissions-table-wrapper">
Expand Down
7 changes: 5 additions & 2 deletions config/locales/views/evaluations/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ en:
evaluation: "Evaluation"
title: "Evaluation for %{series}"
explanation_title: "How do I evaluate a submission?"
explanation_no_grading_html: "<p>In the summary table, you can click on the <i class='mdi mdi-chevron-right mdi-18'></i> icon next to an exercise. You will then be taken to an incompleted submission of a random student. If you wish, you can leave feedback on the code and mark the submission as completed. Note that you can also mark a submission as completed without leaving feedback. To get started, we already marked all exercises without submissions as completed.</p><p>You can find more information in <a href='https://docs.dodona.be/en/guides/teachers/grading/#het-evaluatieoverzicht'>our documentation</a>.</p><p>As a reminder, students will <b>not</b> be able to see the feedback you have given until you click the 'release feedback' button below.</p>"
explanation_yes_grading_html: "<p>In the summary table, you can click on the <i class='mdi mdi-chevron-right mdi-18'></i> icon next to an exercise. You will then be taken to an incomplete submission of a random student. If you wish, you can leave feedback on the code and assign scores using the score items you configured. Note that you can also assign scores without leaving feedback. You can not give scores to exercises without submissions, these are automatically assigned a 0 in the grade overview.</p><p>You can find information in <a href='https://docs.dodona.be/en/guides/teachers/grading/#het-evaluatieoverzicht'>our documentation</a>.</p><p>As a reminder, students will <b>not</b> be able to see the feedback and scores until you click the 'Release feedback' button below.</p>"
explanation_no_grading_html: "<p>In the summary table, you can click on the 'Evaluate' button next to an exercise. You will then be taken to an incompleted submission of a random student. If you wish, you can leave feedback on the code and mark the submission as completed. Note that you can also mark a submission as completed without leaving feedback. To get started, we already marked all exercises without submissions as completed.</p><p>You can find more information in <a href='https://docs.dodona.be/en/guides/teachers/grading/#het-evaluatieoverzicht'>our documentation</a>.</p><p>As a reminder, students will <b>not</b> be able to see the feedback you have given until you click the 'release feedback' button below.</p>"
explanation_yes_grading_html: "<p>In the summary table, you can click on the 'Evaluate' button next to an exercise. You will then be taken to an incomplete submission of a random student. If you wish, you can leave feedback on the code and assign scores using the score items you configured. Note that you can also assign scores without leaving feedback. You can not give scores to exercises without submissions, these are automatically assigned a 0 in the grade overview.</p><p>You can find information in <a href='https://docs.dodona.be/en/guides/teachers/grading/#het-evaluatieoverzicht'>our documentation</a>.</p><p>As a reminder, students will <b>not</b> be able to see the feedback and scores until you click the 'Release feedback' button below.</p>"
release: Release feedback
unrelease: Unrelease feedback
deadline_html: This evaluation of <b>%{users} students</b> contains the submissions of <b>%{exercises} exercises</b> with <b>%{deadline}</b> as a deadline.
Expand Down Expand Up @@ -96,3 +96,6 @@ en:
remove_user_consequences: "If this student already received feedback, that feedback will be deleted. Are you sure you want to remove this student?"
user_progress:
not_submitted: Not submitted
exercises_progress_table:
evaluate: Evaluate
evaluated: Evaluated
7 changes: 5 additions & 2 deletions config/locales/views/evaluations/nl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ nl:
evaluation: "Evaluatie"
title: "Evaluatie voor %{series}"
explanation_title: "Hoe evalueer ik een oplossing?"
explanation_no_grading_html: "<p>In de samenvattende tabel kan je naast een oefening op <i class='mdi mdi-chevron-right mdi-18'></i> klikken. Je komt dan op een nog te bekijken oplossing van een willekeurige student terecht. Vervolgens kan je indien gewenst feedback op de code geven en de oplossing als afgewerkt markeren. Bemerk dat je ook een oplossing als afgewerkt kan markeren zonder feedback achter te laten. We hebben alvast alle oefeningen zonder ingediende oplossing als afgewerkt gemarkeerd.</p><p>Meer informatie kan je in <a href='https://docs.dodona.be/nl/guides/teachers/grading/#het-evaluatieoverzicht'>onze documentatie</a> vinden.</p><p>Ter herinnering, de studenten krijgen de gegeven feedback <b>niet</b> automatisch te zien, daarvoor dien je hieronder op 'Feedback vrijgeven' te klikken.</p>"
explanation_yes_grading_html: "<p>In de samenvattende tabel kan je naast een oefening op <i class='mdi mdi-chevron-right mdi-18'></i> klikken. Je komt dan op een nog te bekijken oplossing van een willekeurige student terecht. Vervolgens kan je indien gewenst feedback op de code geven en scores toekennen via de scoreonderdelen die je geconfigureerd hebt. Je kan de scores ook invullen zonder feedback achter te laten. Oefeningen zonder ingediende oplossing kan je geen scores geven, deze zullen automatisch een 0 krijgen in het puntenoverzicht.</p><p>Meer informatie kan je in <a href='https://docs.dodona.be/nl/guides/teachers/grading/#het-evaluatieoverzicht'>onze documentatie</a> vinden.</p><p>Ter herinnering, de studenten krijgen de gegeven feedback en scores <b>niet</b> automatisch te zien, daarvoor dien je hieronder op 'Feedback vrijgeven' te klikken.</p>"
explanation_no_grading_html: "<p>In de samenvattende tabel kan je naast een oefening op 'Evalueren' klikken. Je komt dan op een nog te bekijken oplossing van een willekeurige student terecht. Vervolgens kan je indien gewenst feedback op de code geven en de oplossing als afgewerkt markeren. Bemerk dat je ook een oplossing als afgewerkt kan markeren zonder feedback achter te laten. We hebben alvast alle oefeningen zonder ingediende oplossing als afgewerkt gemarkeerd.</p><p>Meer informatie kan je in <a href='https://docs.dodona.be/nl/guides/teachers/grading/#het-evaluatieoverzicht'>onze documentatie</a> vinden.</p><p>Ter herinnering, de studenten krijgen de gegeven feedback <b>niet</b> automatisch te zien, daarvoor dien je hieronder op 'Feedback vrijgeven' te klikken.</p>"
explanation_yes_grading_html: "<p>In de samenvattende tabel kan je naast een oefening op 'Evalueren' klikken. Je komt dan op een nog te bekijken oplossing van een willekeurige student terecht. Vervolgens kan je indien gewenst feedback op de code geven en scores toekennen via de scoreonderdelen die je geconfigureerd hebt. Je kan de scores ook invullen zonder feedback achter te laten. Oefeningen zonder ingediende oplossing kan je geen scores geven, deze zullen automatisch een 0 krijgen in het puntenoverzicht.</p><p>Meer informatie kan je in <a href='https://docs.dodona.be/nl/guides/teachers/grading/#het-evaluatieoverzicht'>onze documentatie</a> vinden.</p><p>Ter herinnering, de studenten krijgen de gegeven feedback en scores <b>niet</b> automatisch te zien, daarvoor dien je hieronder op 'Feedback vrijgeven' te klikken.</p>"
users: "studenten"
release: Feedback vrijgeven
unrelease: Feedback verbergen
Expand Down Expand Up @@ -96,3 +96,6 @@ nl:
remove_user_consequences: "Als deze student al feedback gekregen had, dan zal deze feedback verwijderd worden. Ben je zeker dat je deze student wil verwijderen?"
user_progress:
not_submitted: Niet ingediend
exercises_progress_table:
evaluate: Evalueren
evaluated: Geëvalueerd

0 comments on commit b215875

Please sign in to comment.