diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 827fb9f..b3bdccf 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -7,45 +7,39 @@ class CoursesController < ApplicationController def show authorize @course + # query for all projects_instances, owner_type and owner_id for participants table + @course.projects.includes(project_instances: { enrolment: :user }).load + @projects_by_owner = @course.projects.index_by { |p| [p.owner_type, p.owner_id] } + @student_list = @course.students @description = @course.course_description @lecturers = @course.lecturers - @group_list = @course.grouped? ? @course.project_groups.to_a : [] @lecturer_enrolment = @course.enrolments.find_by(user: current_user, role: :lecturer) @current_user_enrolment = @course.enrolments.find_by(user: current_user) @topic_list = policy_scope(@course.topics, policy_scope_class: TopicPolicy::Scope) @my_topics = @topic_list.where(owner: current_user) - # SET STUDENT PROJECTS - projects_ownerships = @course.projects.approved - .where(owner_type: 'User') - .pluck('owner_id') - - @students_with_projects = @student_list.select do |student| - projects_ownerships.include?(student.id) - end - - @students_without_projects = @student_list.reject do |student| - projects_ownerships.include?(student.id) - end + # set students projects + projects_ownerships = @course.projects.approved.where(owner_type: 'User').pluck('owner_id') - @filtered_group_list = filtered_group_list - @filtered_student_list = filtered_student_list - @my_student_projects = [] - @incoming_proposals = [] + @students_with_projects = @student_list.select { |s| projects_ownerships.include?(s.id) } + @students_without_projects = @student_list.reject { |s| projects_ownerships.include?(s.id) } if @course.grouped? @group = current_user.project_groups.find_by(course: @course) - - @project = (@course.projects.find_by(owner_type: 'ProjectGroup', owner_id: @group.id) if @group) + @project = @projects_by_owner[['ProjectGroup', @group.id]] if @group + @group_list = @course.project_groups.includes(project_group_members: :user).to_a else @group = nil @project = @course.projects.find_by(owner_type: 'User', owner_id: current_user.id) + @group_list = [] end + # view instances for incoming topics and my_student_projects @current_status = @project&.current_status || 'not_submitted' - + @my_student_projects = [] + @incoming_proposals = [] if @current_user_enrolment&.coordinator? supervisor_enrolment = @lecturer_enrolment || @current_user_enrolment @my_student_projects = @course.projects.supervised_by(supervisor_enrolment).approved @@ -55,13 +49,29 @@ def show @incoming_proposals = @course.projects.where(enrolment: @current_user_enrolment).proposals end - # SET LECTURER CAPACITY + # view instances for participants_table + @filtered_group_list = filtered_group_list + @filtered_student_list = filtered_student_list + + @show_all = params[:show_all] == 'true' + @total_group_count = @filtered_group_list.count + @total_student_count = @filtered_student_list.count + @total_count = @course.grouped? ? @total_group_count : @total_student_count + @total_count = @course.grouped? ? @filtered_group_list.count : @filtered_student_list.count + + unless @show_all + @filtered_group_list = @filtered_group_list.first(Rails.application.config.participants_pagination_threshold) + @filtered_student_list = @filtered_student_list.first(Rails.application.config.participants_pagination_threshold) + end + + @displayed_count = @course.grouped? ? @filtered_group_list.count : @filtered_student_list.count + @lecturer_capacity_info = {} @lecturers.each do |lecturer| @lecturer_capacity_info[lecturer.id] = @course.lecturer_capacity(lecturer) end - return unless request.headers['HX-Request'] && params[:status_filter].present? + return unless request.headers['HX-Request'] render partial: 'participants_table', locals: { @@ -250,9 +260,11 @@ def profile @participant_type = params[:participant_type] @participant_id = params[:participant_id] @course = Course.find(params[:id]) - @grouped = @course.grouped + @course.projects.includes(project_instances: { enrolment: :user }).load + @projects_by_owner = @course.projects.index_by { |p| [p.owner_type, p.owner_id] } + if @participant_type == 'group' @group = @course.project_groups.find(@participant_id) @members = @group.project_group_members.includes(:user) @@ -512,6 +524,8 @@ def create_lecturer_enrolments(lecturer_emails, parent_course, unregistered_lect end end + # CSV_export helpers + def generate_csv_export template_fields = @course.project_template&.project_template_fields&.order(:id) || [] @@ -624,6 +638,8 @@ def get_project_details_values(current_instance, template_fields) end end + # Lecturer capcity helpers + def lecturer_approved_proposals_count(lecturer, course) lecturer_enrolment = course.enrolments.find_by(user: lecturer, role: :lecturer) return 0 unless lecturer_enrolment @@ -653,6 +669,8 @@ def lecturer_capacity_info(lecturer, course) } end + # Filter Project by status helpers + def students_by_status(status, student_list, students_with_projects, students_without_projects, course) return [] unless student_list.present? @@ -699,18 +717,122 @@ def groups_by_status(status, group_list, course) end end + # Participants Table Filters helpers + + def search_groups(group_list, query) + downcased_query = query.downcase + + group_list.select do |group| + project = participant_project(group, 'ProjectGroup') + + group_name_match = group.group_name.downcase.include?(downcased_query) + member_match = group.project_group_members.any? do |member| + member.user.username.downcase.include?(downcased_query) + end + title_match = project&.current_title&.downcase&.include?(downcased_query) || false + + group_name_match || member_match || title_match + end + end + + def search_students(student_list, query) + downcased_query = query.downcase + + student_list.select do |student| + project = participant_project(student, 'User') + + name_match = student.username.downcase.include?(downcased_query) + id_match = student.student_id&.downcase&.include?(downcased_query) || false + title_match = project&.current_title&.downcase&.include?(downcased_query) || false + + name_match || id_match || title_match + end + end + + def sort_descending? + params[:sort_dir] == 'desc' + end + + def participant_project(item, owner_type) + @projects_by_owner[[owner_type, item.id]] + end + + def sort_value_for_group(group) + project = participant_project(group, 'ProjectGroup') + case params[:sort_by] + when 'status' + Project::STATUS_SORT_ORDER.fetch(project&.current_status || 'not_submitted', 99) + when 'project_title' + project&.current_title&.downcase || '' + when 'supervisor' + project&.supervisor&.username&.downcase || '' + else + group.group_name.downcase + end + end + + def sort_value_for_student(student) + project = participant_project(student, 'User') + case params[:sort_by] + when 'status' + Project::STATUS_SORT_ORDER.fetch(project&.current_status || 'not_submitted', 99) + when 'project_title' + project&.current_title&.downcase || '' + when 'supervisor' + project&.supervisor&.username&.downcase || '' + else + student.username.downcase + end + end + + def lecturer_enrolment_filter + return nil unless params[:lecturer_filter].present? && params[:lecturer_filter] != 'all' + + @course.enrolments.find_by(user_id: params[:lecturer_filter], role: :lecturer) + end + + def supervised_owner_ids(owner_type) + enrolment = lecturer_enrolment_filter + return nil unless enrolment + + @course.projects.supervised_by(enrolment).where(owner_type: owner_type).pluck(:owner_id) + end + def filtered_group_list - group_list = if params[:status_filter].present? && params[:status_filter] != 'all' - @course.groups_with_status(params[:status_filter], @group_list) - else - @group_list - end - group_list.sort_by(&:group_name) + group_list = @group_list + + if (ids = supervised_owner_ids('ProjectGroup')) + group_list = group_list.select { |g| ids.include?(g.id) } + end + + if params[:status_filter].present? && params[:status_filter] != 'all' + group_list = @course.groups_with_status(params[:status_filter], group_list) + end + + if params[:search_query].present? + group_list = search_groups(group_list, params[:search_query]) + end + + sorted_list = group_list.sort_by { |group| sort_value_for_group(group) } + sort_descending? ? sorted_list.reverse : sorted_list end def filtered_student_list - return @student_list unless params[:status_filter].present? && params[:status_filter] != 'all' + student_list = @student_list + + if (ids = supervised_owner_ids('User')) + student_list = student_list.select { |s| ids.include?(s.id) } + end + + if params[:status_filter].present? && params[:status_filter] != 'all' + student_list = @course.students_with_status(params[:status_filter], student_list) + end + + if params[:search_query].present? + student_list = search_students(student_list, params[:search_query]) + end - @course.students_with_status(params[:status_filter], @student_list) + sorted_list = student_list.sort_by { |student| sort_value_for_student(student) } + sort_descending? ? sorted_list.reverse : sorted_list end end diff --git a/app/controllers/participants_controller.rb b/app/controllers/participants_controller.rb index 5a4e7fc..4a182f0 100644 --- a/app/controllers/participants_controller.rb +++ b/app/controllers/participants_controller.rb @@ -1,36 +1,78 @@ class ParticipantsController < ApplicationController def index @course = Course.find(params[:course_id]) + authorize @course + + # query for all projects_instances, owner_type and owner_id for participants table + @course.projects.includes(project_instances: { enrolment: :user }).load + @projects_by_owner = @course.projects.index_by { |p| [p.owner_type, p.owner_id] + @student_list = @course.students - @group_list = @course.grouped? ? @course.project_groups.to_a : [] + + if @course.grouped? + @group_list = @course.project_groups.includes(project_group_members: :user).to_a + else + @group_list = [] + end + + projects_ownerships = @course.projects.approved.where(owner_type: 'User').pluck('owner_id') @filtered_group_list = filtered_group_list @filtered_student_list = filtered_student_list - projects_ownerships = @course.projects.approved - .where(owner_type: 'User') - .pluck('owner_id') + @students_with_projects = @student_list.select { |s| projects_ownerships.include?(s.id) } + @students_without_projects = @student_list.reject { |s| projects_ownerships.include?(s.id) } - @students_with_projects = @student_list.select do |student| - projects_ownerships.include?(student.id) + @show_all = params[:show_all] == 'true' + @total_count = @course.grouped? ? @filtered_group_list.count : @filtered_student_list.count + + unless @show_all + @filtered_group_list = @filtered_group_list.first(Rails.application.config.participants_pagination_threshold) + @filtered_student_list = @filtered_student_list.first(Rails.application.config.participants_pagination_threshold) end - @students_without_projects = @student_list.reject do |student| - projects_ownerships.include?(student.id) + @displayed_count = @course.grouped? ? @filtered_group_list.count : @filtered_student_list.count end end private + def lecturer_enrolment_filter + return nil unless params[:lecturer_filter].present? && params[:lecturer_filter] != 'all' + + @course.enrolments.find_by(user_id: params[:lecturer_filter], role: :lecturer) + end + + def supervised_owner_ids(owner_type) + enrolment = lecturer_enrolment_filter + return nil unless enrolment + + @course.projects.supervised_by(enrolment).where(owner_type: owner_type).pluck(:owner_id) + end + def filtered_group_list - return @group_list unless params[:status_filter].present? && params[:status_filter] != 'all' + group_list = @group_list - @course.groups_with_status(params[:status_filter], @group_list) + if (ids = supervised_owner_ids('ProjectGroup')) + group_list = group_list.select { |g| ids.include?(g.id) } + end + + if params[:status_filter].present? && params[:status_filter] != 'all' + group_list = @course.groups_with_status(params[:status_filter], group_list) + end + + return group_list.sort_by(&:group_name) end def filtered_student_list - return @student_list unless params[:status_filter].present? && params[:status_filter] != 'all' + student_list = @student_list + + if (ids = supervised_owner_ids('User')) + student_list = student_list.select { |s| ids.include?(s.id) } + end + + return student_list unless params[:status_filter].present? && params[:status_filter] != 'all' - @course.students_with_status(params[:status_filter], @student_list) + @course.students_with_status(params[:status_filter], student_list) end end diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb index 991fc8f..600e973 100644 --- a/app/helpers/courses_helper.rb +++ b/app/helpers/courses_helper.rb @@ -1,39 +1,25 @@ module CoursesHelper - def group_project_for(group, course) - @group_projects_cache ||= {} - @group_projects_cache[group.id] ||= course.projects - .find_by(owner_type: 'ProjectGroup', owner_id: group.id) + def group_project_for(group, _course) + @projects_by_owner[['ProjectGroup', group.id]] end - def student_project_for(student, course) - @student_projects_cache ||= {} - @student_projects_cache[student.id] ||= course.projects - .find_by(owner_type: 'User', owner_id: student.id) + def student_project_for(student, _course) + @projects_by_owner[['User', student.id]] end def group_status(group, course) - project = group_project_for(group, course) - return 'not_submitted' unless project - - project.current_status + group_project_for(group, course)&.current_status || 'not_submitted' end def student_status(student, course) - project = student_project_for(student, course) - return 'not_submitted' unless project - - project.current_status + student_project_for(student, course)&.current_status || 'not_submitted' end def participants_exceed?(course) - return false unless course.students.present? - - course.students.count > Rails.application.config.participants_threshold + course.students.size > Rails.application.config.participants_threshold end def supervisors_exceed?(course) - return false unless course.supervisors.present? - - course.supervisors.count > Rails.application.config.supervisors_threshold + course.supervisors.size > Rails.application.config.supervisors_threshold end end diff --git a/app/models/project.rb b/app/models/project.rb index 1ab9e0e..053abce 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -32,6 +32,8 @@ class Project < ApplicationRecord before_validation :set_ownership_type + STATUS_SORT_ORDER = { 'rejected' => 0, 'redo' => 1, 'pending' => 2, 'not_submitted' => 3, 'approved' => 4 }.freeze + def supervisor return nil unless enrolment_id.present? diff --git a/app/views/courses/_participants.html.erb b/app/views/courses/_participants.html.erb index ccbe0cb..324eaa3 100644 --- a/app/views/courses/_participants.html.erb +++ b/app/views/courses/_participants.html.erb @@ -6,12 +6,41 @@ w-full bg-[#f8f9fa] rounded-[10px] p-5 shadow-sm border border-gray-100 "> - <%# --- Header & Filter Controls --- %> + <%# --- Header, Search Bar and Filters --- %>
-

Participants

+
+

Participants

+ + <%# Search Bar %> + <% unless participants_exceed?(@course) && !local_assigns[:fullpage] %> +
+ + + + +
+ <% end %> +
<% if participants_exceed?(@course) && !local_assigns[:fullpage] %> <%= link_to "Show all participants", @@ -22,16 +51,59 @@
+ " id="filter-controls"> +
+ + <%# Lecturer Filter %> +
+ + + +
+ +
+
+ + <%# Status Filter %>
-
- - + + Loading...
+
<% end %> @@ -136,19 +187,19 @@
Total Groups: - <%= @filtered_group_list&.count || 0 %> + <%= @total_group_count || 0 %>
Total Students: - <%= @filtered_student_list&.count || 0 %> + <%= @total_student_count || 0 %>
<% else %>
Total Students: - <%= @filtered_student_list&.count || 0 %> + <%= @total_student_count || 0 %>
<% end %>
diff --git a/app/views/courses/_participants_table.html.erb b/app/views/courses/_participants_table.html.erb index 4e5baa2..7b6c704 100644 --- a/app/views/courses/_participants_table.html.erb +++ b/app/views/courses/_participants_table.html.erb @@ -1,3 +1,41 @@ +<%# sort column helper %> +<% sort_column = ->(column, label, icon, align: :left, width: nil) do %> + <% is_active = params[:sort_by] == column %> + <% next_dir = (is_active && params[:sort_dir] == 'asc') ? 'desc' : 'asc' %> + +
+ <%= image_tag icon, class: "w-5 h-5 opacity-70" %> + <%= label %> + <% if is_active %> + <% if params[:sort_dir] == 'desc' %> + + + + <% else %> + + + + <% end %> + <% else %> + + + + <% end %> +
+ +<% end %> +
@@ -5,68 +43,39 @@ <%# --- Table Header --- %> - - - + + <%# 1. Group Name / Student Name %> + <% if @course.grouped? %> + <% sort_column.call('group_name', 'Group Name', 'alphabet.svg', width: 'w-[14.28%]') %> + <% else %> + <% sort_column.call('student_name', 'Student Name', 'alphabet.svg', width: 'w-1/3') %> + <% end %> + + <%# 2. Members / Student ID %> + <% if @course.grouped? %> + + <% else %> + <% sort_column.call('student_id', 'Student ID', 'person_grey.svg', width: 'w-[14.28%]') %> + <% end %> <%# 3. Project Title %> - + <% sort_column.call('project_title', 'Project Title', 'project.svg', width: 'w-1/5') %> <%# 4. Status %> - + <% sort_column.call('status', 'Status', 'status_grey.svg', align: :center, width: 'w-1/8') %> <%# 5. Supervisor %> - + <% sort_column.call('supervisor', 'Supervisor', 'person_grey.svg', width: 'w-1/4') %> + @@ -98,9 +107,8 @@ <%# 2. Members List %> <%# 5. Supervisor %> - + <%# 5. Supervisor %> -
-
- <%= image_tag "alphabet.svg", class: "w-5 h-5 opacity-70" %> - <%= @course.grouped? ? "Group Name" : "Student Name" %> -
-
-
- <%= image_tag "person_grey.svg", class: "w-5 h-5 opacity-70" %> - <%= @course.grouped? ? "Members" : "Student ID" %> -
-
+
+ <%= image_tag "person_grey.svg", class: "w-5 h-5 opacity-70" %> + Members +
+
-
- <%= image_tag "project.svg", class: "w-5 h-5 opacity-70" %> - Project Title -
-
-
- <%= image_tag "status_grey.svg", class: "w-5 h-5 opacity-70" %> - Status -
-
-
- <%= image_tag "person_grey.svg", class: "w-5 h-5 opacity-70" %> - Supervisor -
-
    - <% group.project_group_members.includes(:user).each do |member| %> + <% group.project_group_members.each do |member| %>
  • - <%= link_to participant_profile_course_path(@course, member.user.id, 'student'), class: "inline group/member hover:text-gray-900" do %>
    <%= member.user.username || "-" %> <% end %> - <%# Pending Badge (Inline, flows after name) %> <% unless member.user.has_registered %>
    - <%# --- STATUS BADGE --- %> <% project_status = status.to_s.downcase %> <% badge_classes, dot_class = case project_status - when "approved" - ["bg-emerald-100 text-emerald-800 border-emerald-200", "bg-emerald-600"] - when "pending" - ["bg-blue-100 text-blue-800 border-blue-200", "bg-blue-600"] - when "redo" - ["bg-orange-100 text-orange-800 border-orange-200", "bg-orange-600"] - when "rejected" - ["bg-rose-100 text-rose-800 border-rose-200", "bg-rose-600"] - else - ["bg-gray-100 text-gray-600 border-gray-200", "bg-gray-500"] + when "approved" then ["bg-emerald-100 text-emerald-800 border-emerald-200", "bg-emerald-600"] + when "pending" then ["bg-blue-100 text-blue-800 border-blue-200", "bg-blue-600"] + when "redo" then ["bg-orange-100 text-orange-800 border-orange-200", "bg-orange-600"] + when "rejected" then ["bg-rose-100 text-rose-800 border-rose-200", "bg-rose-600"] + else ["bg-gray-100 text-gray-600 border-gray-200", "bg-gray-500"] end %> - - <% link_target = - group_project.present? ? course_project_path(@course, group_project) : "#" %> - + <% link_target = group_project.present? ? course_project_path(@course, group_project) : "#" %> + + "> <%= status.humanize %> - <%# --- PROGRESS INDICATOR (Below Badge) --- %> <% if use_progress_updates && group_project.present? && project_status == "approved" %> <% count = group_project.progress_updates.count %> - <%= count %> - updates + <%= count %> updates <% end %> -
+ <% if supervisor.present? %> <%= link_to course_lecturer_path(@course, supervisor), class: "flex items-center justify-start gap-2 group/sup" do %>
<%= supervisor.username %> @@ -305,53 +295,38 @@ <%# 4. Status (Student) %>
- <%# --- STATUS BADGE --- %> - <% project_status = status.to_s.downcase %> <% badge_classes, dot_class = case project_status - when "approved" - ["bg-emerald-100 text-emerald-800 border-emerald-200", "bg-emerald-600"] - when "pending" - ["bg-blue-100 text-blue-800 border-blue-200", "bg-blue-600"] - when "redo" - ["bg-orange-100 text-orange-800 border-orange-200", "bg-orange-600"] - when "rejected" - ["bg-rose-100 text-rose-800 border-rose-200", "bg-rose-600"] - else - ["bg-gray-100 text-gray-600 border-gray-200", "bg-gray-500"] + when "approved" then ["bg-emerald-100 text-emerald-800 border-emerald-200", "bg-emerald-600"] + when "pending" then ["bg-blue-100 text-blue-800 border-blue-200", "bg-blue-600"] + when "redo" then ["bg-orange-100 text-orange-800 border-orange-200", "bg-orange-600"] + when "rejected" then ["bg-rose-100 text-rose-800 border-rose-200", "bg-rose-600"] + else ["bg-gray-100 text-gray-600 border-gray-200", "bg-gray-500"] end %> + <% link_target = student_project.present? ? course_project_path(@course, student_project) : "#" %> - <% link_target = - student_project.present? ? course_project_path(@course, student_project) : "#" %> - - + "> <%= status.humanize %> - <%# --- PROGRESS INDICATOR (Below Badge) --- %> <% if use_progress_updates && student_project.present? && project_status == "approved" %> <% count = student_project.progress_updates.count %> - <%= count %> - updates + <%= count %> updates <% end %> -
+ <% if supervisor.present? %> <%= link_to course_lecturer_path(@course, supervisor), class: "flex items-center justify-start gap-2 group/sup" do %>
<%= supervisor.username %> @@ -393,4 +366,32 @@
+ + <%# --- Pagination Bar --- %> + <% if @total_count > 0 %> +
+ + Showing <%= @displayed_count %> + of <%= @total_count %> + <%= @course.grouped? ? 'groups' : 'students' %> + + + <% if !@show_all && @total_count > @displayed_count %> + + <% end %> +
+ <% end %>
+ diff --git a/config/application.rb b/config/application.rb index d115942..ae8f567 100644 --- a/config/application.rb +++ b/config/application.rb @@ -28,5 +28,6 @@ class Application < Rails::Application # view constants thresholds config.participants_threshold = 500 config.supervisors_threshold = 100 + config.participants_pagination_threshold = 25 end end