Skip to content
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

TaskContributions #1103

Merged
merged 111 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
111 commits
Select commit Hold shift + click to select a range
bd60bc9
Prerequisites for task contrib
florian-str Jul 5, 2023
56106ac
Added reference to folder for task templates
florian-str Jul 5, 2023
832b545
Continue work on task contributions
florian-str Jul 18, 2023
95a3500
Implemented suggestions for contrib
florian-str Jul 22, 2023
f4292d8
Implemented discard_changes
florian-str Jul 25, 2023
1f32f5c
Implemented basic approval
florian-str Jul 25, 2023
aa9a3ca
Removed unused code
florian-str Jul 26, 2023
15c396c
Initial data transfer for contrib
florian-str Jul 26, 2023
b2c6810
Added update method for contrib
florian-str Aug 2, 2023
d185ce5
Added file_parent_id validation
florian-str Aug 16, 2023
7371e7d
Implemented validations + tests for helpers
florian-str Aug 28, 2023
75a9bb8
Fixed parent_id not copied
florian-str Aug 28, 2023
6974800
Implemented tests for ModelSolution parent validation
florian-str Aug 28, 2023
704e946
Added model tests + wip contrib controller spec
florian-str Sep 5, 2023
aa9dc39
Fixed controller tests + moved code to model
florian-str Sep 6, 2023
b9c640f
Fixed spec issues
florian-str Oct 11, 2023
715d147
Fixed failing tests
florian-str Oct 25, 2023
be3fcb4
Renamed task in TaskContribution to indicate modifying direction
florian-str Oct 25, 2023
2e9f99b
Manually fixed rebase
florian-str Oct 25, 2023
6c9cf23
Handled rubocop offenses
florian-str Oct 25, 2023
92839cb
Added ability check + frontend fix for contrib
florian-str Nov 2, 2023
de0974c
Added ability specs for task_contrib
florian-str Nov 8, 2023
20f5c3e
Fixed test_form rendering
florian-str Nov 9, 2023
87758b0
Fixed failing tests for task_contrib
florian-str Nov 15, 2023
53f9301
Refactored contrib to taskcontrib for controller
florian-str Nov 15, 2023
3bc3c2c
Revert "Refactored contrib to taskcontrib for controller"
florian-str Nov 20, 2023
65085d3
Corrected method for locales
florian-str Nov 20, 2023
363a294
Added locale for breadcrumbs
florian-str Nov 20, 2023
bf1dca9
Added contrib mailer
florian-str Nov 20, 2023
b618ea2
Added mailer for contrib controller
florian-str Nov 21, 2023
c73cbb0
Improved test coverage for contributions_controller
florian-str Nov 23, 2023
07d4d58
Fixed file merge for contrib
florian-str Nov 23, 2023
8707fe0
Fixed model_solutions not copied
florian-str Nov 30, 2023
bb96a21
Fixed file creation in contrib
florian-str Dec 13, 2023
30f5888
Fix missing new lines and locales
florian-str Dec 13, 2023
9aba4a3
Renamed contributions_controller to task_contributions_controller
florian-str Jan 22, 2024
9af40e4
Moved to task_contrib pundit
florian-str Feb 7, 2024
84b2d8e
Fixed nil check for contrib redirect
florian-str Feb 7, 2024
a94f913
Refactor Task Contributions (#1230)
MrSerth Feb 8, 2024
01c8c6e
Added tests for ContribController + TransferValues
florian-str Feb 20, 2024
e2266ba
Fixed spec for #approve_changes
florian-str Feb 21, 2024
de8a1b3
Moved transfer_linked_files test to shared example
florian-str Feb 21, 2024
b020936
Fixed rebase issues
florian-str Feb 21, 2024
93253d1
Simplified TransferValues
florian-str Mar 10, 2024
ad312a5
Fixed ParentValidation
florian-str Mar 11, 2024
5fff8d2
Added tests for recursive TransferValues
florian-str Mar 11, 2024
1c6b02c
Added TaskContributionMailer tests
florian-str Mar 11, 2024
13f2351
Added license check
florian-str Mar 12, 2024
d363754
Added redirect if user has open contrib
florian-str Apr 10, 2024
1d4a214
Rebased to latest
florian-str Apr 10, 2024
015e965
Translated locales
florian-str Apr 17, 2024
946dd6d
Moved erb mailer to slim
florian-str Apr 22, 2024
f719f84
Fixed task + contribution mismatch
florian-str Apr 28, 2024
6fadd7d
Moved new contrib to edit redirect to view
florian-str Apr 28, 2024
b43ffa1
Fixed unique_pending_contribution error
florian-str May 12, 2024
0b85a1b
Fixed factory error
florian-str May 12, 2024
bbdea11
Added to_s for contrib
florian-str May 13, 2024
ec2c4c1
Fixed slim-lint
florian-str May 13, 2024
81eae62
Added foreign key for parent_id
florian-str May 13, 2024
cb705f1
Added contrib index
florian-str May 14, 2024
aa743db
Fixed permissions for task contrib
florian-str May 28, 2024
8816689
Added dedicated contribution view
florian-str May 29, 2024
a2e5b95
Corrected contrib closing workflow
florian-str May 29, 2024
b1364ff
Fixed linting + test condition
florian-str May 29, 2024
c10281a
Fixed parent_ids
florian-str Jun 10, 2024
d30e231
Fixed error message
florian-str Jun 12, 2024
debc9bc
Minor improvements
florian-str Jun 12, 2024
f1c7a12
Fixed naming issue
florian-str Jun 12, 2024
e59c7bc
Moved mailer tests + translations
florian-str Jun 26, 2024
fed9da4
Removed inline styles
florian-str Jun 26, 2024
5aa2d23
Fixed linting
florian-str Jun 26, 2024
ea9320a
Minor UX improvements
florian-str Jun 26, 2024
e9b571e
Minor translation fixes
florian-str Jul 9, 2024
d156a05
Fixed rebase issues
florian-str Jul 9, 2024
59faf8d
Fixed style issues
florian-str Jul 9, 2024
1c5c107
Added contrib information
florian-str Jul 10, 2024
9aebfd6
Updated locales for task_contribution
florian-str Jul 18, 2024
0b0a115
Added send_ prefix for contrib mailers
florian-str Jul 18, 2024
7305706
Fixed wrong span number in contrib + added missing disabled tooltip
florian-str Jul 18, 2024
66d8cb4
Added task_contrib index styling
florian-str Jul 18, 2024
0b1ba29
Fixed locales + linting
florian-str Jul 18, 2024
e67921c
Split discard_changes and reject + fixed missing metadata + fixed mis…
florian-str Jul 31, 2024
7772583
Added tests + metadata transfer + minor improvments
florian-str Aug 16, 2024
7053fda
Fixed issue caused by refactoring
florian-str Aug 16, 2024
b58fe17
Correctly render task form during validation errors when a new file h…
MrSerth Sep 1, 2024
5f85f3b
Fix behavior for invalid task contributions
MrSerth Sep 1, 2024
f33230e
Allow editing an existing contribution
MrSerth Sep 1, 2024
a775086
Allow admins to perform any action on contributions
MrSerth Sep 1, 2024
751d85d
Allow new task contributions without changes to attached files
MrSerth Sep 1, 2024
40dff8b
Correctly apply changes to attached files when accepting a contribution
MrSerth Sep 1, 2024
59c8c96
Disallow changes to the polymorphic file_type when transferring values
MrSerth Sep 1, 2024
d1980c0
Refactor exclusion of attributes during transfer
MrSerth Sep 1, 2024
fabd32c
Refactor transfer specs to pass class names rather than symbols
MrSerth Sep 1, 2024
5618c6f
Remove inaccessible task contributions from tasks index page
MrSerth Sep 1, 2024
45c5fc6
Include task name in task contributions index page
MrSerth Sep 1, 2024
79e5507
Reverse ordering of task contributions to show newest first
MrSerth Sep 1, 2024
768c1f9
Fix capitalization of action headers for task show
MrSerth Sep 1, 2024
cc99a6b
Used prefixed enum for contribution status
MrSerth Sep 1, 2024
f19d721
Aggregate all contribution-related policies
MrSerth Sep 2, 2024
b2a60be
Use dynamic routing for task views
MrSerth Sep 2, 2024
91b673b
Add pagination for TaskContributions#index
MrSerth Sep 2, 2024
17ef278
Fixed TaskContribution index policy
florian-str Sep 11, 2024
3fe091c
Fixed semantic of TaskContributionsControllerSpec
florian-str Sep 11, 2024
637d374
Added test for apply_contribution
florian-str Sep 11, 2024
276dc1f
Fixed linting
florian-str Sep 11, 2024
3f2e953
Tests for duplicating file attachments
florian-str Sep 11, 2024
16a63a0
Fixed index permission check + changed example metadata
florian-str Sep 14, 2024
79f247e
Added feature spec for attach_parent_blob when file is not changed
florian-str Sep 14, 2024
b040c96
Explicitly remove file attachment if changed after validation failure
MrSerth Sep 25, 2024
1d93fac
Added feature spec for attach_parent_blob when file is changed
florian-str Oct 2, 2024
5289a69
Fix TransferValues for recently changed files association
MrSerth Oct 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions app/controllers/concerns/task_parameters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

module TaskParameters
private

def file_params
%i[id content attachment path name internal_description mime_type use_attached_file used_by_grader visible usage_by_lms xml_id _destroy
parent_id parent_blob_id]
end

def test_params
[:id, :title, :testing_framework_id, :description, :internal_description, :test_type, :xml_id, :validity, :timeout, :_destroy,
:parent_id, {files_attributes: file_params}]
end

def model_solution_params
[:id, :description, :internal_description, :xml_id, :_destroy, :parent_id, {files_attributes: file_params}]
end

def task_params
params.require(:task).permit(:title, :description, :internal_description, :parent_uuid, :language, :license_id,
:programming_language_id, :access_level, files_attributes: file_params, tests_attributes: test_params,
model_solutions_attributes: model_solution_params, label_names: []).tap {|parameters| fix_nested_files_params(parameters) }
end

def group_tasks_params
params.permit(group_tasks: {group_ids: []})[:group_tasks]
end

def import_confirm_params
params.permit(:import_id, :subfile_id, :import_type)
end

def fix_nested_files_params(parameters)
# The task_params might contain incomplete attributes for files, which we must fix before using the parameters.
# Those file attributes can be attached directly to the task, to any of test or to any model solution.
# For each of the potential file attributes, we must check if the attachment attribute should present and re-add it if missing.
# This behavior is needed, since SimpleForm does not keep the (empty) attachment attribute if no file got attached by the user.
# Otherwise, an old file version would be linked to the task, test or model solution after a failed model validation.
# When this happens, the old file content is used alongside the name of the new file, which is confusing for the user.

# Fix file attributes directly linked to the task.
fix_attachment_params(parameters)

# Fix file attributes linked to tests. The tests are stored in a hash with the test number as the key.
parameters[:tests_attributes]&.each_value {|test_parameters| fix_attachment_params(test_parameters) }

# Fix file attributes linked to model solutions. The model solutions are stored in a hash with the model solution number as the key.
parameters[:model_solutions_attributes]&.each_value {|model_solution_parameters| fix_attachment_params(model_solution_parameters) }
end

def fix_attachment_params(parameters)
# Files are stored in a hash with the file number as the key.
parameters[:files_attributes]&.each_value do |file|
# Do not modify the given file attributes if an attachment key is already present.
# This is needed to keep the attachment if the user sent a file.
next if file.key?(:attachment)
# Do not modify the given file attributes if the parent_blob_id is present.
# This ensures that an attachment is kept even if the user did not send a file.
# For example, a file has been stored previously and the user only wants to update unrelated attributes.
# As soon as the user chose a file manually and validations fail, the parent_blob_id is left empty.
# Then, on a second try, the (updated) file is no longer linked to the previous file version.
next if file[:parent_blob_id].present?

# If none of the above conditions is met, the user did not send a file despite previously making changes.
# In this case, we must re-add the attachment attribute and set it to `nil` to indicate the missing file.
file[:attachment] = nil
end
end
end
112 changes: 112 additions & 0 deletions app/controllers/task_contributions_controller.rb
florian-str marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# frozen_string_literal: true

class TaskContributionsController < ApplicationController
include TaskParameters

before_action :load_and_authorize_base_task
before_action :load_and_authorize_task_contribution, except: %i[index create new]

def approve_changes
if @task.apply_contribution(@task_contribution)
TaskContributionMailer.send_approval_info(@task_contribution).deliver_later
redirect_to @task, notice: t('.success')
else
redirect_to [@task, @task_contribution], alert: t('.error')
end
end

def discard_changes
duplicate = @task_contribution.decouple
if duplicate
redirect_to duplicate, notice: t('.success')
else
redirect_to [@task, @task_contribution], alert: t('.error')
end
end

def reject_changes
duplicate = @task_contribution.decouple
if duplicate
TaskContributionMailer.send_rejection_info(@task_contribution, duplicate).deliver_later
redirect_to @task, notice: t('.success')
else
redirect_to [@task, @task_contribution], alert: t('.error')
end
end

def index
@task_contributions = @task.contributions.order(created_at: :desc).paginate(page: params[:page], per_page: per_page_param)
raise Pundit::NotAuthorizedError unless policy(@task_contributions).index? base: @task
end

def show
@task = @task_contribution.suggestion

@files = @task.files
@tests = @task.tests
@model_solutions = @task.model_solutions

@user_rating = @task.ratings&.find_by(user: current_user)&.rating
render 'tasks/show'
end

def new
raise Pundit::NotAuthorizedError if current_user.nil?

@task_contribution = TaskContribution.new_for(@task, current_user)
authorize @task_contribution

@task = @task_contribution.suggestion
end

# The function should render the edit form used by TaskController
def edit
@task = @task_contribution.suggestion
render 'tasks/edit'
end

def create # rubocop:disable Metrics/AbcSize
@task_contribution = TaskContribution.new(suggestion_attributes: task_params, base: @task)
@task_contribution.suggestion.assign_attributes(
user: current_user,
access_level: :private,
meta_data: @task.meta_data,
submission_restrictions: @task.submission_restrictions,
external_resources: @task.external_resources,
grading_hints: @task.grading_hints
)
authorize @task_contribution

if @task_contribution.save(context: :force_validations)
TaskContributionMailer.send_contribution_request(@task_contribution).deliver_later
redirect_to [@task, @task_contribution], notice: t('common.notices.object_created', model: TaskContribution.model_name.human)
else
@task = @task_contribution.suggestion
render 'tasks/new'
end
end

def update
MrSerth marked this conversation as resolved.
Show resolved Hide resolved
@task_contribution.suggestion.assign_attributes(task_params.except(:parent_uuid))
if @task_contribution.save(context: :force_validations)
redirect_to [@task, @task_contribution], notice: t('common.notices.object_updated', model: TaskContribution.model_name.human)
else
@task = @task_contribution.suggestion
render 'tasks/edit', danger: t('common.errors.changes_not_saved', model: TaskContribution.model_name.human)
end
end

private

def load_and_authorize_base_task
@task = Task.find(params[:task_id])
authorize @task, :show?
end

def load_and_authorize_task_contribution
@task_contribution = TaskContribution.find(params[:id])
raise Pundit::NotAuthorizedError unless @task_contribution.base == @task

authorize @task_contribution
end
end
35 changes: 8 additions & 27 deletions app/controllers/tasks_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
require 'zip'

class TasksController < ApplicationController # rubocop:disable Metrics/ClassLength
include TaskParameters
before_action :load_and_authorize_task, except: %i[index new create import_start import_confirm import_uuid_check import_external]
before_action :load_and_authorize_account_link, only: %i[export_external_start export_external_check export_external_confirm]
before_action :only_authorize_action, only: %i[import_start import_confirm import_uuid_check import_external]

before_action :handle_search_params, only: :index
before_action :set_search, only: [:index]
before_action :redirect_task_contribution, only: %i[create update edit show]
prepend_before_action :set_user_for_api_request, only: %i[import_uuid_check import_external]
skip_before_action :verify_authenticity_token, only: %i[import_uuid_check import_external]
skip_before_action :require_user!, only: %i[show download]
Expand Down Expand Up @@ -237,6 +239,12 @@ def only_authorize_action
authorize Task
end

def redirect_task_contribution
return unless @task.present? && @task.contribution?

redirect_to action: action_name, controller: 'task_contributions', task_id: @task.parent, id: @task.task_contribution
MrSerth marked this conversation as resolved.
Show resolved Hide resolved
end

def set_search # rubocop:disable Metrics/AbcSize
search = params[:q]
@req_labels = []
Expand Down Expand Up @@ -268,33 +276,6 @@ def handle_search_params
save_search_params
end

def file_params
%i[id content attachment path name internal_description mime_type use_attached_file used_by_grader visible usage_by_lms xml_id _destroy]
end

def test_params
[:id, :title, :testing_framework_id, :description, :internal_description, :test_type, :xml_id, :validity, :timeout, :_destroy,
{files_attributes: file_params}]
end

def model_solution_params
[:id, :description, :internal_description, :xml_id, :_destroy, {files_attributes: file_params}]
end

def task_params
params.require(:task).permit(:title, :description, :internal_description, :parent_uuid, :language, :license_id,
:programming_language_id, :access_level, files_attributes: file_params, tests_attributes: test_params,
model_solutions_attributes: model_solution_params, label_names: [])
end

def group_tasks_params
params.require(:group_tasks).permit(group_ids: [])
end

def import_confirm_params
params.permit(:import_id, :subfile_id, :import_type)
end

def set_user_for_api_request
authorization_header = request.headers['Authorization']
api_key = authorization_header&.split&.second
Expand Down
9 changes: 0 additions & 9 deletions app/mailers/access_request_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,4 @@ def send_access_request(user, admin, group)
mail(to: @admin.email, subject: t('groups.access_request_mailer.subject', user: @user.name, group: @group.name))
end
end

def send_contribution_request(author, exercise, user)
@author = author
@exercise = exercise
@user = user
I18n.with_locale(@author.preferred_locale || I18n.default_locale) do
mail(to: @author.email, subject: t('tasks.access_request_mailer.subject', user: @user.name, task: @exercise.title))
end
end
end
33 changes: 33 additions & 0 deletions app/mailers/task_contribution_mailer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# frozen_string_literal: true

class TaskContributionMailer < ApplicationMailer
def send_contribution_request(task_contrib)
@task_author = task_contrib.base.user
@task_contrib = task_contrib
@contrib_author = task_contrib.suggestion.user
I18n.with_locale(@task_author.preferred_locale || I18n.default_locale) do
mail(to: @task_author.email,
subject: t('task_contributions.mailer.contribution_request.subject_message', contrib_author: @contrib_author,
task: @task_contrib.base.title))
end
end

def send_approval_info(task_contrib)
@task_contrib = task_contrib
@contrib_author = task_contrib.suggestion.user
I18n.with_locale(@contrib_author.preferred_locale || I18n.default_locale) do
mail(to: @contrib_author.email,
subject: t('task_contributions.mailer.approval_info.subject_message', task: @task_contrib.base.title))
end
end

def send_rejection_info(task_contrib, duplicate)
@task_contrib = task_contrib
@contrib_author = task_contrib.suggestion.user
@duplicate = duplicate
I18n.with_locale(@contrib_author.preferred_locale || I18n.default_locale) do
mail(to: @contrib_author.email,
subject: t('task_contributions.mailer.rejection_info.subject_message', task: @task_contrib.base.title))
end
end
end
11 changes: 11 additions & 0 deletions app/models/concerns/file_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,15 @@ def files

association(:files).reader.records.filter {|file| file.fileable == self }
end

# For the transfer of files between two records, we need to overwrite the getter and setter methods for the files association.
# We cannot use the `files` method defined above, since it would return a copy of the respective files as an array.
# Hence, we define a dedicated getter and setter method for the files association used in the context of task contributions.
def files_collection
association(:files).reader
end

def files_collection=(files)
association(:files).writer files
end
end
12 changes: 12 additions & 0 deletions app/models/concerns/parent_validation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module ParentValidation
def parent_validation_check
return if parent_id.nil?

parent_entry = self.class.find_by(id: parent_id)
if parent_entry.blank? || !parent_entry.task.parent_of?(task)
errors.add(:parent_id, :invalid)
end
end
end
44 changes: 44 additions & 0 deletions app/models/concerns/transfer_values.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module TransferValues
def transfer_attributes(other) # rubocop:disable Metrics/AbcSize
assign_attributes(other.attributes.except(*excluded_attributes_from_transfer))

if is_a?(TaskFile) && other.attachment.attached?
attachment.attach(other.attachment.blob)
self.use_attached_file = 'true'
elsif !is_a?(TaskFile)
transfer_multiple_entities(files_collection, other.files_collection)
end
end

def transfer_multiple_entities(targets, others)
# Remove deleted elements
targets.each do |target|
unless others.exists?(parent_id: target.id)
targets.delete(target)
end
end
# Adding new or modified elements
others.to_a.each do |other|
if targets.exists?(other.parent_id)
old_entity = targets.find {|target| target.id == other.parent_id } # Required to force searching a copy in memory instead of db
old_entity.transfer_attributes(other)
else
targets.append(other.duplicate(set_parent_id: false))
end
end
end

private

def excluded_attributes_from_transfer
exclude = %w[id parent_id created_at task_id]
if is_a?(TaskFile)
exclude.append('fileable_id', 'fileable_type')
else
exclude.append('xml_id')
end
exclude
end
end
21 changes: 18 additions & 3 deletions app/models/model_solution.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,28 @@

class ModelSolution < ApplicationRecord
include FileConcern
include TransferValues
include ParentValidation

belongs_to :task, autosave: true, inverse_of: :model_solutions
belongs_to :parent, class_name: 'ModelSolution', optional: true
has_many :files, as: :fileable, class_name: 'TaskFile', dependent: :destroy
accepts_nested_attributes_for :files, allow_destroy: true
validates :xml_id, presence: true
validates :parent_id, uniqueness: {scope: :task}, if: -> { parent_id.present? }
validate :parent_validation_check
# TODO: For new tasks, this validation is currently useless, because the validation is performed
# before the task is saved (and thus the task_id is not yet known, i.e., is NULL). Therefore,
# one can create a **new task** with a test that has the same xml_id as another test of the same task.
# TODO: This validation is currently useless on new records, because the uuid is generated after validation
validates :xml_id, uniqueness: {scope: :task_id}

def duplicate
dup.tap do |model_solutions|
model_solutions.files = files.map(&:duplicate)
def duplicate(set_parent_id: true)
dup.tap do |model_solution|
model_solution.files = files.map {|file| file.duplicate(set_parent_id:) }
if set_parent_id
model_solution.parent_id = id
end
end
end
end
Loading
Loading