diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fa30d85..cdab582 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -82,17 +82,25 @@ def new @lecturer_options = Enrolment.where(course: @course, role: :lecturer).includes(:user) - @field_values = {} - - # Optionally preselect topic or own proposal - topic_id = params[:topic_id].presence || params[:based_on_topic] + # Choose Supervisor + @lecturers = @course.lecturers + @lecturer_capacity_info = {} + @lecturers.each do |lecturer| + @lecturer_capacity_info[lecturer.id] = @course.lecturer_capacity(lecturer) + end - return unless topic_id.present? + @field_values = {} - if topic_id.to_s.start_with?('own_proposal_') - @selected_topic_id = topic_id - elsif @course.topics.exists?(id: topic_id) - @selected_topic_id = topic_id + if params[:topic_id].present? + @selected_topic = @course.topics.find_by(id: params[:topic_id]) + if @selected_topic + latest_instance = @selected_topic.current_instance + @field_values = latest_instance.project_instance_fields.each_with_object({}) do |f, h| + h[f.project_template_field_id.to_i] = f.value + end + end + elsif params[:lecturer_id].present? + @selected_lecturer = @course.lecturers.find_by(id: params[:lecturer_id]) end end @@ -135,29 +143,20 @@ def create raise StandardError, 'You already have a project' if has_project end - raise StandardError, 'Please choose a lecturer and topic' if params[:based_on_topic].blank? - - if params[:based_on_topic].start_with?('own_proposal_') - # Extract lecturer ID from value - lecturer_id = params[:based_on_topic].split('_').last.to_i - - # Find lecturer enrolment for course - supervisor_enrolment = Enrolment.find_by(user_id: lecturer_id, course_id: @course.id, role: :lecturer) - - raise StandardError unless supervisor_enrolment - else - # Treat as topic_id - topic = Topic.find_by(id: params[:based_on_topic], course: @course) + topic = Topic.find_by(id: params[:based_on_topic], course: @course) if params[:based_on_topic].present? + lecturer_id = params[:lecturer_id].presence - raise StandardError unless topic + raise StandardError, 'Please choose a lecturer or topic' if topic.nil? && lecturer_id.nil? - # Set supervisor enrolment to the owner of the topic (assuming you want this) - topic_owner = topic&.owner - raise StandardError unless topic_owner.is_a?(User) + if topic + topic_owner = topic.owner + raise StandardError, 'Topic has no valid owner' unless topic_owner.is_a?(User) supervisor_enrolment = Enrolment.find_by(user_id: topic_owner.id, course_id: @course.id, role: :lecturer) - - raise StandardError unless supervisor_enrolment + raise StandardError, 'Could not find supervisor enrolment' unless supervisor_enrolment + else + supervisor_enrolment = Enrolment.find_by(user_id: lecturer_id, course_id: @course.id, role: :lecturer) + raise StandardError, 'Could not find supervisor enrolment' unless supervisor_enrolment end @project = Project.create!( @@ -256,34 +255,25 @@ def update end end - @instance.title = params[:fields].values.first if !is_approved && params[:fields].present? && @instance.title.blank? - - @instance.last_edit_time = Time.current - @instance.last_edit_by = current_user.id - @instance.save! + topic = Topic.find_by(id: params[:based_on_topic], course: @course) if params[:based_on_topic].present? + lecturer_id = params[:lecturer_id].presence - unless is_approved - raise StandardError, 'Please choose a lecturer and topic' if params[:based_on_topic].blank? - - 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 - - @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) + raise StandardError, 'Please choose a lecturer or topic' if topic.nil? && lecturer_id.nil? - supervisor_enrolment = Enrolment.find_by(user_id: topic.owner.id, course_id: @course.id, role: :lecturer) - raise StandardError, 'Supervisor enrolment missing' unless supervisor_enrolment - - @instance.update!(source_topic: topic) - end + if topic + raise StandardError, 'Topic has no valid owner' unless topic.owner.is_a?(User) + supervisor_enrolment = Enrolment.find_by(user_id: topic.owner.id, course_id: @course.id, role: :lecturer) + + raise StandardError, 'Could not find supervisor enrolment' unless supervisor_enrolment + @instance.update!(source_topic: topic) + else + supervisor_enrolment = Enrolment.find_by(user_id: lecturer_id, course_id: @course.id, role: :lecturer) + raise StandardError, 'Could not find supervisor enrolment' unless supervisor_enrolment + @instance.update!(source_topic_id: nil) + end @project.update!(enrolment: supervisor_enrolment) end - end rescue StandardError => e redirect_to course_project_path(@course, @project), alert: "Project update failed: #{e.message}" return diff --git a/app/javascript/application.js b/app/javascript/application.js index 7d1db6d..0d80407 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -7,3 +7,7 @@ import "htmx.org"; document.addEventListener("turbo:load", function () { window.htmx.process(document.body); }); + +Turbo.config.drive.confirmationMethod = (message, element, submitter) => { + return Promise.resolve(window.confirm(message)); +}; \ No newline at end of file diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js index fccc1dc..ddb5d1a 100644 --- a/app/javascript/controllers/index.js +++ b/app/javascript/controllers/index.js @@ -1,4 +1,4 @@ // Import and register all your controllers from the importmap via controllers/**/*_controller import { application } from "controllers/application"; import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"; -eagerLoadControllersFrom("controllers", application); +eagerLoadControllersFrom("controllers", application); \ No newline at end of file diff --git a/app/javascript/controllers/proposal_selection_controller.js b/app/javascript/controllers/proposal_selection_controller.js new file mode 100644 index 0000000..93dab54 --- /dev/null +++ b/app/javascript/controllers/proposal_selection_controller.js @@ -0,0 +1,80 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["lecturerPanel", "topicPanel", "hiddenLecturerId", "hiddenTopicId"] + + connect() { + // Restore initial state from server-rendered hidden fields on page load + const lecturerId = this.hiddenLecturerIdTarget.value + const topicId = this.hiddenTopicIdTarget.value + + if (lecturerId) { + this.#disablePanel(this.topicPanelTarget) + } else if (topicId) { + this.#disablePanel(this.lecturerPanelTarget) + } + } + + selectLecturer(event) { + const button = event.currentTarget + const lecturerId = button.dataset.lecturerId + const isAlreadySelected = this.hiddenLecturerIdTarget.value === lecturerId + + if (isAlreadySelected) { + // Deselect + this.hiddenLecturerIdTarget.value = "" + this.#markSelected(button, false) + this.#enablePanel(this.topicPanelTarget) + } else { + // Warn if topic is already selected + if (this.hiddenTopicIdTarget.value) { + if (!confirm("You'll lose your selected topic. Continue?")) return + this.hiddenTopicIdTarget.value = "" + } + + this.hiddenLecturerIdTarget.value = lecturerId + this.#clearAllLecturerSelections() + this.#markSelected(button, true) + this.#disablePanel(this.topicPanelTarget) + this.#enablePanel(this.lecturerPanelTarget) + } + } + + // Called by the "Clear" link next to the lecturer heading + clearLecturer(event) { + event.preventDefault() + this.hiddenLecturerIdTarget.value = "" + this.#clearAllLecturerSelections() + this.#enablePanel(this.topicPanelTarget) + } + + // Private + + #clearAllLecturerSelections() { + this.lecturerPanelTarget + .querySelectorAll("button[data-lecturer-id]") + .forEach(btn => this.#markSelected(btn, false)) + } + + #markSelected(button, selected) { + const check = button.querySelector("[data-selected-check]") + + if (selected) { + button.classList.add("border-blue-400", "ring-2", "ring-blue-100") + button.classList.remove("border-gray-200", "hover:border-gray-400") + if (check) check.classList.remove("hidden") + } else { + button.classList.remove("border-blue-400", "ring-2", "ring-blue-100") + button.classList.add("border-gray-200", "hover:border-gray-400") + if (check) check.classList.add("hidden") + } + } + + #disablePanel(panel) { + panel.classList.add("opacity-50", "pointer-events-none") + } + + #enablePanel(panel) { + panel.classList.remove("opacity-50", "pointer-events-none") + } +} \ No newline at end of file diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index bad27ba..619ba87 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -49,7 +49,7 @@ def show? end def create? - student || coordinator + student && !has_existing_project? end def update? @@ -110,4 +110,8 @@ def any_free_edit_fields? &.project_template_fields &.exists?(free_edit: true) || false end + + def has_existing_project? + course.projects.owned_by_user(user).exists? + end end diff --git a/app/views/courses/_topic_card.html.erb b/app/views/courses/_topic_card.html.erb index 97b79a6..94fb809 100644 --- a/app/views/courses/_topic_card.html.erb +++ b/app/views/courses/_topic_card.html.erb @@ -21,7 +21,7 @@ if local_assigns[:lecturer].present? course_lecturer_topic_path(course, lecturer, topic) else - course_topic_path(@course, topic) + course_topic_path(@course, topic, from_new_project: params[:from_new_project]) end card_classes = diff --git a/app/views/projects/new.html.erb b/app/views/projects/new.html.erb index 384649f..e9c0d20 100644 --- a/app/views/projects/new.html.erb +++ b/app/views/projects/new.html.erb @@ -63,87 +63,197 @@ " >
- Warning: - Switching to a lecturer's topic will autofill that topic's details. (You will lose any existing details in the fields!) + +
+ Choose one option below to start your proposal.
-