Skip to content

Commit fae6c4b

Browse files
authored
Merge pull request #299 from hackerspacemmu/feat/free-edit-field
Free edit fields
2 parents 0b9150f + 1a10f29 commit fae6c4b

16 files changed

Lines changed: 245 additions & 195 deletions

app/controllers/project_templates_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def project_template_params
3838
params.require(:project_template).permit(
3939
:description,
4040
project_template_fields_attributes: [
41-
:id, :label, :hint, :field_type, :applicable_to, :_destroy, { options: [] }, :required
41+
:id, :label, :hint, :field_type, :applicable_to, :_destroy, { options: [] }, :required, :free_edit
4242
]
4343
)
4444
end

app/controllers/projects_controller.rb

Lines changed: 43 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -208,90 +208,84 @@ def create
208208

209209
def update
210210
authorize @project || Project.new(course: @course)
211+
is_approved = @project.approved?
211212

212-
has_supervisor_comment = false
213-
@project.project_instances.last.comments.each do |comment|
214-
if comment.user_id == @project.supervisor.id
215-
has_supervisor_comment = true
216-
break
217-
end
218-
end
213+
last_instance = @project.project_instances.last
214+
has_supervisor_comment = last_instance.comments.exists?(user_id: @project.supervisor.id)
219215

220216
previous_supervisor_id = @project.supervisor.id
221217
new_instance_created = false
222218

223219
begin
224220
ActiveRecord::Base.transaction do
225-
return unless @project.editable?
221+
raise StandardError, 'This project is locked and cannot be edited.' unless @project.editable? || is_approved
226222

227223
@instance = @project.instance_to_edit(
228224
created_by: current_user,
229225
has_supervisor_comment: has_supervisor_comment
230226
)
231227
new_instance_created = @instance.new_record?
232228

233-
# Set title
234-
title_field_id = params[:fields].keys.first if params[:fields].present?
235-
@instance.title = params[:fields][title_field_id] if title_field_id.present?
236-
237-
# Timestamps
238-
@instance.last_edit_time = Time.current
239-
@instance.last_edit_by = current_user.id
229+
if new_instance_created
230+
previous_instance = @project.project_instances.where.not(id: nil).order(version: :desc).first
240231

241-
raise StandardError unless @instance.save
232+
previous_instance&.project_instance_fields&.each do |old_field|
233+
@instance.project_instance_fields.build(
234+
project_template_field_id: old_field.project_template_field_id,
235+
value: old_field.value
236+
)
237+
end
238+
end
242239

243-
raise StandardError unless params[:fields].present?
240+
raise StandardError, 'No field data provided.' if params[:fields].blank?
244241

245242
params[:fields].each do |field_id, value|
246-
existing_field = ProjectInstanceField.find_by(
247-
project_template_field_id: field_id,
248-
instance: @instance
249-
)
243+
template_field = ProjectTemplateField.find(field_id)
244+
245+
next if is_approved && !template_field.free_edit
250246

251-
if existing_field
252-
existing_field.update!(value: value)
247+
field = @instance.project_instance_fields.find { |f| f.project_template_field_id == field_id.to_i }
248+
249+
if field
250+
field.value = value
253251
else
254-
@instance.project_instance_fields.create!(
252+
@instance.project_instance_fields.build(
255253
project_template_field_id: field_id,
256254
value: value
257255
)
258256
end
259257
end
260258

261-
raise StandardError, 'Please choose a lecturer and topic' if params[:based_on_topic].blank?
262-
263-
# 2 formats, PROJECT_ID or own_proposal_LECTURER_ID
264-
if params[:based_on_topic].start_with?('own_proposal_')
265-
# Extract lecturer ID from value
266-
lecturer_id = params[:based_on_topic].split('_').last.to_i
259+
@instance.title = params[:fields].values.first if !is_approved && params[:fields].present? && @instance.title.blank?
267260

268-
# Find lecturer enrolment for course
269-
supervisor_enrolment = Enrolment.find_by(id: lecturer_id, course_id: @course.id, role: :lecturer)
270-
271-
raise StandardError unless supervisor_enrolment
261+
@instance.last_edit_time = Time.current
262+
@instance.last_edit_by = current_user.id
263+
@instance.save!
272264

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

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

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

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

284-
raise StandardError unless supervisor_enrolment
281+
@instance.update!(source_topic: topic)
282+
end
285283

286-
@instance.update!(source_topic: topic)
284+
@project.update!(enrolment: supervisor_enrolment)
287285
end
288-
289-
@project.project_instances.last.update!(
290-
enrolment: supervisor_enrolment
291-
)
292286
end
293-
rescue StandardError
294-
redirect_to course_project_path(@course, @project), alert: 'Project update failed'
287+
rescue StandardError => e
288+
redirect_to course_project_path(@course, @project), alert: "Project update failed: #{e.message}"
295289
return
296290
end
297291

app/javascript/project_template_fields.js

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -46,55 +46,68 @@ document.addEventListener("turbo:load", function () {
4646
<tr
4747
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"
4848
data-field-index="${index}"
49+
data-controller="project-template-fields"
4950
>
5051
<td class="block lg:table-cell px-6 lg:pl-12 lg:pr-6 py-5 whitespace-nowrap align-top">
5152
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Field Label</span>
5253
<div class="relative">
5354
<textarea
54-
name="project_template[project_template_fields_attributes][${index}][label]"
55-
placeholder="e.g. Project Title"
56-
rows="1"
57-
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"
58-
data-controller="textarea-resize"
59-
data-action="input->textarea-resize#resize"
55+
name="project_template[project_template_fields_attributes][${index}][label]"
56+
placeholder="e.g. Project Title"
57+
rows="1"
58+
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"
59+
data-controller="textarea-resize"
60+
data-action="input->textarea-resize#resize"
6061
></textarea>
6162
62-
<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">
63-
<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>
63+
<button
64+
type="button"
65+
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"
66+
title="Remove Field"
67+
>
68+
<svg class="h-5 w-5 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
69+
<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" />
70+
</svg>
6471
</button>
6572
</div>
6673
</td>
6774
6875
<td class="block lg:table-cell px-6 py-5 align-top">
6976
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Hint Text</span>
70-
<textarea name="project_template[project_template_fields_attributes][${index}][hint]"
71-
placeholder="Instructions..."
72-
rows="1"
73-
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"
74-
data-controller="textarea-resize"
75-
data-action="input->textarea-resize#resize"
77+
<textarea
78+
name="project_template[project_template_fields_attributes][${index}][hint]"
79+
placeholder="Instructions..."
80+
rows="1"
81+
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"
82+
data-controller="textarea-resize"
83+
data-action="input->textarea-resize#resize"
7684
></textarea>
7785
</td>
7886
7987
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
8088
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Field Type</span>
81-
<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">
89+
<select
90+
name="project_template[project_template_fields_attributes][${index}][field_type]"
91+
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"
92+
>
8293
<option value="">Select field type</option>
8394
${generateFieldTypeOptions()}
8495
</select>
8596
</td>
8697
8798
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
8899
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Applicable To</span>
89-
<select name="project_template[project_template_fields_attributes][${index}][applicable_to]"
90-
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">
100+
<select
101+
name="project_template[project_template_fields_attributes][${index}][applicable_to]"
102+
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"
103+
>
91104
${generateApplicableToOptions()}
92105
</select>
93106
</td>
94107
95-
<td class="block lg:table-cell px-6 py-5 align-top">
108+
<td class="block lg:table-cell px-6 py-5 align-top" data-project-template-fields-target="optionsContainer">
96109
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Options</span>
97-
110+
98111
<div class="options-section hidden w-full">
99112
<button type="button"
100113
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"
@@ -106,20 +119,36 @@ document.addEventListener("turbo:load", function () {
106119
</div>
107120
108121
<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">
109-
<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>
122+
<svg class="mr-1.5 h-4 w-4 pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor">
123+
<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" />
124+
</svg>
110125
Remove Field
111126
</button>
127+
<input type="hidden" name="project_template[project_template_fields_attributes][${index}][_destroy]" value="0" class="destroy-flag">
112128
</td>
113129
114-
<td class="block lg:table-cell px-6 py-5 align-top">
130+
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
115131
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Required</span>
116-
<input type="hidden"
117-
name="project_template[project_template_fields_attributes][${index}][required]"
118-
value="false">
119-
<input type="checkbox"
120-
name="project_template[project_template_fields_attributes][${index}][required]"
121-
value="true"
122-
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">
132+
<div class="flex items-center h-10">
133+
<input type="hidden" name="project_template[project_template_fields_attributes][${index}][required]" value="0">
134+
<input type="checkbox"
135+
name="project_template[project_template_fields_attributes][${index}][required]"
136+
value="1"
137+
class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer">
138+
<label class="ml-2 text-sm text-gray-600 lg:hidden">Field is required</label>
139+
</div>
140+
</td>
141+
142+
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
143+
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Free Edit</span>
144+
<div class="flex items-center h-10" title="Allow editing after approval">
145+
<input type="hidden" name="project_template[project_template_fields_attributes][${index}][free_edit]" value="0">
146+
<input type="checkbox"
147+
name="project_template[project_template_fields_attributes][${index}][free_edit]"
148+
value="1"
149+
class="h-5 w-5 rounded border-gray-300 text-green-600 focus:ring-green-500 cursor-pointer">
150+
<label class="ml-2 text-sm text-gray-600 lg:hidden">Editable after approval</label>
151+
</div>
123152
</td>
124153
</tr>
125154
`;

app/models/project.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,11 +71,13 @@ def editable?
7171
end
7272

7373
def instance_to_edit(created_by:, has_supervisor_comment:)
74-
if rejected? || redo? || (pending? && has_supervisor_comment)
74+
if approved? || rejected? || redo? || (pending? && has_supervisor_comment)
7575
project_instances.build(
76-
version: project_instances.count + 1,
76+
version: (project_instances.maximum(:version) || 0) + 1,
7777
created_by: created_by,
78-
enrolment: enrolment
78+
enrolment: enrolment,
79+
title: current_title,
80+
status: (status if approved?)
7981
)
8082
else
8183
# If approved and pending (no supervisor comment) dont create new instance

app/policies/project_policy.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,13 @@ def create?
5353
end
5454

5555
def update?
56-
project_owner && !approved
56+
return true if project_owner && !approved
57+
58+
return true if project_owner && approved && any_free_edit_fields?
59+
60+
return true if coordinator
61+
62+
false
5763
end
5864

5965
def change_status?
@@ -98,4 +104,10 @@ def student
98104
def approved
99105
record.status.to_s == 'approved'
100106
end
107+
108+
def any_free_edit_fields?
109+
record.course.project_template
110+
&.project_template_fields
111+
&.exists?(free_edit: true) || false
112+
end
101113
end

app/views/project_templates/_field_row.html.erb

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,14 +155,22 @@
155155
</td>
156156

157157
<%# --- Required --- %>
158-
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-center">
159-
<span
160-
class="
161-
lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block
162-
"
163-
>Required</span>
164-
<%= field_form.check_box :required,
165-
class:
166-
"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" %>
158+
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
159+
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Required</span>
160+
<div class="flex items-center h-10">
161+
<%= field_form.check_box :required,
162+
class: "h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500 cursor-pointer" %>
163+
<label class="ml-2 text-sm text-gray-600 lg:hidden">Field is required</label>
164+
</div>
165+
</td>
166+
167+
<%# --- Free Edit --- %>
168+
<td class="block lg:table-cell px-6 py-5 whitespace-nowrap align-top">
169+
<span class="lg:hidden text-xs font-bold text-gray-500 uppercase tracking-wide mb-1 block">Free Edit</span>
170+
<div class="flex items-center h-10" title="Allow editing after approval">
171+
<%= field_form.check_box :free_edit,
172+
class: "h-5 w-5 rounded border-gray-300 text-green-600 focus:ring-green-500 cursor-pointer" %>
173+
<label class="ml-2 text-sm text-gray-600 lg:hidden">Editable after approval</label>
174+
</div>
167175
</td>
168176
</tr>

0 commit comments

Comments
 (0)