Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion app/controllers/project_templates_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def project_template_params
params.require(:project_template).permit(
:description,
project_template_fields_attributes: [
:id, :label, :hint, :field_type, :applicable_to, :_destroy, { options: [] }, :required
:id, :label, :hint, :field_type, :applicable_to, :_destroy, { options: [] }, :required, :free_edit
]
)
end
Expand Down
92 changes: 43 additions & 49 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -208,90 +208,84 @@ def create

def update
authorize @project || Project.new(course: @course)
is_approved = @project.approved?

has_supervisor_comment = false
@project.project_instances.last.comments.each do |comment|
if comment.user_id == @project.supervisor.id
has_supervisor_comment = true
break
end
end
last_instance = @project.project_instances.last
has_supervisor_comment = last_instance.comments.exists?(user_id: @project.supervisor.id)

previous_supervisor_id = @project.supervisor.id
new_instance_created = false

begin
ActiveRecord::Base.transaction do
return unless @project.editable?
raise StandardError, 'This project is locked and cannot be edited.' unless @project.editable? || is_approved

@instance = @project.instance_to_edit(
created_by: current_user,
has_supervisor_comment: has_supervisor_comment
)
new_instance_created = @instance.new_record?

# Set title
title_field_id = params[:fields].keys.first if params[:fields].present?
@instance.title = params[:fields][title_field_id] if title_field_id.present?

# Timestamps
@instance.last_edit_time = Time.current
@instance.last_edit_by = current_user.id
if new_instance_created
previous_instance = @project.project_instances.where.not(id: nil).order(version: :desc).first

raise StandardError unless @instance.save
previous_instance&.project_instance_fields&.each do |old_field|
@instance.project_instance_fields.build(
project_template_field_id: old_field.project_template_field_id,
value: old_field.value
)
end
end

raise StandardError unless params[:fields].present?
raise StandardError, 'No field data provided.' if params[:fields].blank?

params[:fields].each do |field_id, value|
existing_field = ProjectInstanceField.find_by(
project_template_field_id: field_id,
instance: @instance
)
template_field = ProjectTemplateField.find(field_id)

next if is_approved && !template_field.free_edit

if existing_field
existing_field.update!(value: value)
field = @instance.project_instance_fields.find { |f| f.project_template_field_id == field_id.to_i }

if field
field.value = value
else
@instance.project_instance_fields.create!(
@instance.project_instance_fields.build(
project_template_field_id: field_id,
value: value
)
end
end

raise StandardError, 'Please choose a lecturer and topic' if params[:based_on_topic].blank?

# 2 formats, PROJECT_ID or own_proposal_LECTURER_ID
if params[:based_on_topic].start_with?('own_proposal_')
# Extract lecturer ID from value
lecturer_id = params[:based_on_topic].split('_').last.to_i
@instance.title = params[:fields].values.first if !is_approved && params[:fields].present? && @instance.title.blank?

# Find lecturer enrolment for course
supervisor_enrolment = Enrolment.find_by(id: lecturer_id, course_id: @course.id, role: :lecturer)

raise StandardError unless supervisor_enrolment
@instance.last_edit_time = Time.current
@instance.last_edit_by = current_user.id
@instance.save!

@instance.update!(source_topic_id: nil)
else
# Treat as topic_id
topic = Topic.find_by(id: params[:based_on_topic], course: @course)
unless is_approved
raise StandardError, 'Please choose a lecturer and topic' if params[:based_on_topic].blank?

raise StandardError unless topic
if params[:based_on_topic].start_with?('own_proposal_')
lecturer_id = params[:based_on_topic].split('_').last.to_i
supervisor_enrolment = Enrolment.find_by(id: lecturer_id, course_id: @course.id, role: :lecturer)
raise StandardError, 'Lecturer not found' unless supervisor_enrolment

raise StandardError unless topic.owner.is_a?(User)
@instance.update!(source_topic_id: nil)
else
topic = Topic.find_by(id: params[:based_on_topic], course: @course)
raise StandardError, 'Topic not found' unless topic && topic.owner.is_a?(User)

supervisor_enrolment = Enrolment.find_by(user_id: topic.owner.id, course_id: @course.id, role: :lecturer)
supervisor_enrolment = Enrolment.find_by(user_id: topic.owner.id, course_id: @course.id, role: :lecturer)
raise StandardError, 'Supervisor enrolment missing' unless supervisor_enrolment

raise StandardError unless supervisor_enrolment
@instance.update!(source_topic: topic)
end

@instance.update!(source_topic: topic)
@project.update!(enrolment: supervisor_enrolment)
end

@project.project_instances.last.update!(
enrolment: supervisor_enrolment
)
end
rescue StandardError
redirect_to course_project_path(@course, @project), alert: 'Project update failed'
rescue StandardError => e
redirect_to course_project_path(@course, @project), alert: "Project update failed: #{e.message}"
return
end

Expand Down
85 changes: 57 additions & 28 deletions app/javascript/project_template_fields.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,55 +46,68 @@ document.addEventListener("turbo:load", function () {
<tr
class="field-row group relative flex flex-col bg-white transition-colors hover:bg-gray-50/50 border-b border-gray-600 last-of-type:border-b-0 lg:table-row lg:border-none"
data-field-index="${index}"
data-controller="project-template-fields"
>
<td class="block lg:table-cell px-6 lg:pl-12 lg:pr-6 py-5 whitespace-nowrap align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Field Label</span>
<div class="relative">
<textarea
name="project_template[project_template_fields_attributes][${index}][label]"
placeholder="e.g. Project Title"
rows="1"
class="block w-full px-3 py-2.5 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm resize-none overflow-hidden"
data-controller="textarea-resize"
data-action="input->textarea-resize#resize"
name="project_template[project_template_fields_attributes][${index}][label]"
placeholder="e.g. Project Title"
rows="1"
class="block w-full px-3 py-2.5 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm resize-none overflow-hidden"
data-controller="textarea-resize"
data-action="input->textarea-resize#resize"
></textarea>

<button type="button" class="remove-field hidden lg:flex items-center justify-center absolute -left-10 top-1/2 -translate-y-1/2 w-8 h-8 text-gray-400 opacity-60 hover:opacity-100 hover:bg-red-50 hover:text-red-600 rounded-md transition-all" title="Remove Field">
<svg class="h-5 w-5 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
<button
type="button"
class="remove-field hidden lg:flex items-center justify-center absolute -left-10 top-1/2 -translate-y-1/2 w-8 h-8 text-gray-400 opacity-60 hover:opacity-100 hover:bg-red-50 hover:text-red-600 rounded-md transition-all"
title="Remove Field"
>
<svg class="h-5 w-5 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</td>

<td class="block lg:table-cell px-6 py-5 align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Hint Text</span>
<textarea name="project_template[project_template_fields_attributes][${index}][hint]"
placeholder="Instructions..."
rows="1"
class="block w-full px-3 py-2.5 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm resize-none overflow-hidden"
data-controller="textarea-resize"
data-action="input->textarea-resize#resize"
<textarea
name="project_template[project_template_fields_attributes][${index}][hint]"
placeholder="Instructions..."
rows="1"
class="block w-full px-3 py-2.5 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm resize-none overflow-hidden"
data-controller="textarea-resize"
data-action="input->textarea-resize#resize"
></textarea>
</td>

<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Field Type</span>
<select name="project_template[project_template_fields_attributes][${index}][field_type]" class="field-type-select block w-full py-2.5 pl-3 pr-8 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm cursor-pointer">
<select
name="project_template[project_template_fields_attributes][${index}][field_type]"
class="field-type-select block w-full py-2.5 pl-3 pr-2.5 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm cursor-pointer"
>
<option value="">Select field type</option>
${generateFieldTypeOptions()}
</select>
</td>

<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Applicable To</span>
<select name="project_template[project_template_fields_attributes][${index}][applicable_to]"
class="block w-full py-2.5 pl-3 pr-8 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm cursor-pointer">
<select
name="project_template[project_template_fields_attributes][${index}][applicable_to]"
class="block w-full py-2.5 pl-3 pr-8 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm cursor-pointer"
>
${generateApplicableToOptions()}
</select>
</td>

<td class="block lg:table-cell px-6 py-5 align-top">
<td class="block lg:table-cell px-6 py-5 align-top" data-project-template-fields-target="optionsContainer">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Options</span>

<div class="options-section hidden w-full">
<button type="button"
class="add-option-btn text-[0.8125rem] text-gray-500 bg-transparent border border-dashed border-gray-300 rounded py-2 px-3 cursor-pointer transition-all duration-150 ease-in-out w-fit hover:text-blue-600 hover:border-blue-500 hover:bg-[#f8f9fa] mt-2"
Expand All @@ -106,20 +119,36 @@ document.addEventListener("turbo:load", function () {
</div>

<button type="button" class="remove-field lg:hidden mt-3 inline-flex items-center text-xs text-red-500 hover:text-red-700 font-medium bg-red-50 px-3 py-2 rounded-md">
<svg class="mr-1.5 h-4 w-4 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
<svg class="mr-1.5 h-4 w-4 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
Remove Field
</button>
<input type="hidden" name="project_template[project_template_fields_attributes][${index}][_destroy]" value="0" class="destroy-flag">
</td>

<td class="block lg:table-cell px-6 py-5 align-top">
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Required</span>
<input type="hidden"
name="project_template[project_template_fields_attributes][${index}][required]"
value="false">
<input type="checkbox"
name="project_template[project_template_fields_attributes][${index}][required]"
value="true"
class="block w-full py-2.5 pl-3 pr-8 border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 sm:text-sm cursor-pointer">
<div class="flex items-center h-10">
<input type="hidden" name="project_template[project_template_fields_attributes][${index}][required]" value="0">
<input type="checkbox"
name="project_template[project_template_fields_attributes][${index}][required]"
value="1"
class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer">
<label class="ml-2 text-sm text-gray-600 lg:hidden">Field is required</label>
</div>
</td>

<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Free Edit</span>
<div class="flex items-center h-10" title="Allow editing after approval">
<input type="hidden" name="project_template[project_template_fields_attributes][${index}][free_edit]" value="0">
<input type="checkbox"
name="project_template[project_template_fields_attributes][${index}][free_edit]"
value="1"
class="h-5 w-5 rounded border-gray-300 text-green-600 focus:ring-green-500 cursor-pointer">
<label class="ml-2 text-sm text-gray-600 lg:hidden">Editable after approval</label>
</div>
</td>
</tr>
`;
Expand Down
8 changes: 5 additions & 3 deletions app/models/project.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,13 @@ def editable?
end

def instance_to_edit(created_by:, has_supervisor_comment:)
if rejected? || redo? || (pending? && has_supervisor_comment)
if approved? || rejected? || redo? || (pending? && has_supervisor_comment)
project_instances.build(
version: project_instances.count + 1,
version: (project_instances.maximum(:version) || 0) + 1,
created_by: created_by,
enrolment: enrolment
enrolment: enrolment,
title: current_title,
status: (status if approved?)
)
else
# If approved and pending (no supervisor comment) dont create new instance
Expand Down
14 changes: 13 additions & 1 deletion app/policies/project_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,13 @@ def create?
end

def update?
project_owner && !approved
return true if project_owner && !approved

return true if project_owner && approved && any_free_edit_fields?

return true if coordinator

false
end

def change_status?
Expand Down Expand Up @@ -98,4 +104,10 @@ def student
def approved
record.status.to_s == 'approved'
end

def any_free_edit_fields?
record.course.project_template
&.project_template_fields
&.exists?(free_edit: true) || false
end
end
26 changes: 17 additions & 9 deletions app/views/project_templates/_field_row.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -155,14 +155,22 @@
</td>

<%# --- Required --- %>
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-center">
<span
class="
lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block
"
>Required</span>
<%= field_form.check_box :required,
class:
"block w-full py-2.5 pl-3 pr-8 border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500 sm:text-sm cursor-pointer" %>
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Required</span>
<div class="flex items-center h-10">
<%= field_form.check_box :required,
class: "h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" %>
<label class="ml-2 text-sm text-gray-600 lg:hidden">Field is required</label>
</div>
</td>

<%# --- Free Edit --- %>
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Free Edit</span>
<div class="flex items-center h-10" title="Allow editing after approval">
<%= field_form.check_box :free_edit,
class: "h-5 w-5 rounded border-gray-300 text-green-600 focus:ring-green-500 cursor-pointer" %>
<label class="ml-2 text-sm text-gray-600 lg:hidden">Editable after approval</label>
</div>
</td>
</tr>
Loading
Loading