From 7749ec424ce0da77a960f15e3a4dce4499ea15a3 Mon Sep 17 00:00:00 2001 From: Kevin Rukundo Date: Thu, 25 Jun 2026 10:45:56 +0200 Subject: [PATCH 1/4] feat(2602): add a message when search results come up empty --- .gitignore | 1 + Gemfile | 1 + Gemfile.lock | 12 + app/components/table_components/table.rb | 4 + app/views/chapters/index.html.erb | 2 +- app/views/groups/index.html.erb | 2 +- app/views/organizations/index.html.erb | 2 +- app/views/students/index.html.erb | 2 +- config/database.yml | 2 + config/locales/en.yml | 5 +- db/structure.sql | 453 +++++++++--------- .../components/table_components/table_spec.rb | 43 ++ 12 files changed, 294 insertions(+), 235 deletions(-) create mode 100644 spec/components/table_components/table_spec.rb diff --git a/.gitignore b/.gitignore index 71ec0f9d1..981d28584 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ yarn-debug.log* /app/assets/builds/* !/app/assets/builds/.keep +prod_db.sql \ No newline at end of file diff --git a/Gemfile b/Gemfile index 926b0ad30..80d7eb598 100644 --- a/Gemfile +++ b/Gemfile @@ -36,6 +36,7 @@ gem 'stimulus-rails' gem 'tailwindcss-rails' gem 'tiddle' gem 'turbo-rails' +gem 'tzinfo-data' gem 'view_component' group :production do diff --git a/Gemfile.lock b/Gemfile.lock index 89493259b..217a6961d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -175,6 +175,7 @@ GEM net-http (~> 0.5) fastimage (2.2.4) ffi (1.17.4) + ffi (1.17.4-x64-mingw-ucrt) fog-aws (3.33.2) base64 (>= 0.2, < 0.4) fog-core (~> 2.6) @@ -198,6 +199,9 @@ GEM google-protobuf (4.33.4) bigdecimal rake (>= 13) + google-protobuf (4.33.4-x64-mingw-ucrt) + bigdecimal + rake (>= 13) has_scope (0.9.0) actionpack (>= 7.0) activesupport (>= 7.0) @@ -279,6 +283,8 @@ GEM nokogiri (1.19.3) mini_portile2 (~> 2.8.2) racc (~> 1.4) + nokogiri (1.19.3-x64-mingw-ucrt) + racc (~> 1.4) oauth2 (2.0.18) faraday (>= 0.17.3, < 4.0) jwt (>= 1.0, < 4.0) @@ -313,6 +319,7 @@ GEM ast (~> 2.4.1) racc pg (1.6.3) + pg (1.6.3-x64-mingw-ucrt) pg_query (6.2.2) google-protobuf (>= 3.25.3) pg_search (2.3.7) @@ -498,6 +505,7 @@ GEM railties (>= 7.0.0) tailwindcss-ruby (~> 4.0) tailwindcss-ruby (4.1.16) + tailwindcss-ruby (4.1.16-x64-mingw-ucrt) thor (1.5.0) tiddle (1.8.1) activerecord (>= 6.1.0) @@ -509,6 +517,8 @@ GEM railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + tzinfo-data (1.2026.2) + tzinfo (>= 1.0.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) unicode-emoji (4.2.0) @@ -539,6 +549,7 @@ GEM PLATFORMS ruby + x64-mingw-ucrt DEPENDENCIES SyslogLogger @@ -602,6 +613,7 @@ DEPENDENCIES tailwindcss-rails tiddle turbo-rails + tzinfo-data view_component webmock diff --git a/app/components/table_components/table.rb b/app/components/table_components/table.rb index 52a972172..7c09a2a25 100644 --- a/app/components/table_components/table.rb +++ b/app/components/table_components/table.rb @@ -16,8 +16,12 @@ class TableComponents::Table < ViewComponent::Base <%= render TableComponents::Column.with_collection(@row_component::columns(**@column_arguments), order_scope_name: @order_scope_name) %> <% if @options[:turbo_id] %>
<% end %> <%= render @row_component.with_collection(@rows, pagy: @pagy, **@row_arguments) %> + <% if @rows.empty? && @options[:empty_message] %> +
<%= @options[:empty_message] %>
+ <% end %> + ERB # rubocop:disable Metrics/ParameterLists diff --git a/app/views/chapters/index.html.erb b/app/views/chapters/index.html.erb index 66e026e38..3c1ac835e 100644 --- a/app/views/chapters/index.html.erb +++ b/app/views/chapters/index.html.erb @@ -8,7 +8,7 @@
-<%= render TableComponents::Table.new(pagy: @pagy, rows: @chapters, row_component: TableComponents::ChapterRow) do |t| +<%= render TableComponents::Table.new(pagy: @pagy, rows: @chapters, row_component: TableComponents::ChapterRow,options: { empty_message: t(:no_chapters_found) }) do |t| t.with_left do %>
diff --git a/app/views/groups/index.html.erb b/app/views/groups/index.html.erb index a92e3ec64..5f5acac22 100644 --- a/app/views/groups/index.html.erb +++ b/app/views/groups/index.html.erb @@ -7,7 +7,7 @@ ].compact) %>
-<%= render TableComponents::Table.new(pagy: @pagy, rows: @groups, row_component: TableComponents::GroupRow) do |t| +<%= render TableComponents::Table.new(pagy: @pagy, rows: @groups, row_component: TableComponents::GroupRow,options: { empty_message: t(:no_groups_found) }) do |t| t.with_left do %>
diff --git a/app/views/organizations/index.html.erb b/app/views/organizations/index.html.erb index b2e4e14a6..9d6425258 100644 --- a/app/views/organizations/index.html.erb +++ b/app/views/organizations/index.html.erb @@ -8,7 +8,7 @@
-<%= render TableComponents::Table.new(pagy: @pagy, rows: @organizations, row_component: TableComponents::OrganizationRow) do |t| +<%= render TableComponents::Table.new(pagy: @pagy, rows: @organizations, row_component: TableComponents::OrganizationRow,options: { empty_message: t(:no_organizations_found) }) do |t| t.with_left do %>
diff --git a/app/views/students/index.html.erb b/app/views/students/index.html.erb index df34e63cb..801eee9ab 100644 --- a/app/views/students/index.html.erb +++ b/app/views/students/index.html.erb @@ -7,7 +7,7 @@ (CommonComponents::ButtonComponent.new(label: t(:add_student), href: new_student_path) if policy(Student).new?) ].compact) %> -<%= render TableComponents::Table.new(pagy: @pagy, rows: @student_rows, row_component: TableComponents::StudentRow) do |t| +<%= render TableComponents::Table.new(pagy: @pagy, rows: @student_rows, row_component: TableComponents::StudentRow, options: { empty_message: t(:no_students_found) }) do |t| t.with_left do %>
diff --git a/config/database.yml b/config/database.yml index a253baef9..cb77f735d 100644 --- a/config/database.yml +++ b/config/database.yml @@ -3,6 +3,8 @@ default: &default pool: 5 timeout: 5000 host: <%= ENV['DATABASE_HOST'] || 'localhost' %> + # port: 5433 + port: 5432 username: <%= ENV['DATABASE_USER'] || 'tracker' %> password: <%= ENV['DATABASE_PASSWORD'] || 'tracker' %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 9dc1b2a63..e701535b3 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -357,6 +357,10 @@ en: health_insurance: Health Insurance health_issues: Health Issues hiv_tested: HIV Tested + no_students_found: "No Students found" + no_groups_found: "No Groups found" + no_chapters_found: "No Chapters found" + no_organizations_found: "No Organizations found" confirm: Confirm import: Import @@ -451,7 +455,6 @@ en: one: MLID other: MLID - errors: messages: extension_whitelist_error: File is not an image diff --git a/db/structure.sql b/db/structure.sql index 9d53667dd..addd1f06a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -24,20 +24,6 @@ CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public; COMMENT ON EXTENSION btree_gist IS 'support for indexing common datatypes in GiST'; --- --- Name: pg_stat_statements; Type: EXTENSION; Schema: -; Owner: - --- - -CREATE EXTENSION IF NOT EXISTS pg_stat_statements WITH SCHEMA public; - - --- --- Name: EXTENSION pg_stat_statements; Type: COMMENT; Schema: -; Owner: - --- - -COMMENT ON EXTENSION pg_stat_statements IS 'track execution statistics of all SQL statements executed'; - - -- -- Name: pgcrypto; Type: EXTENSION; Schema: -; Owner: - -- @@ -249,8 +235,8 @@ SET default_table_access_method = heap; CREATE TABLE public.ar_internal_metadata ( key character varying NOT NULL, value character varying, - created_at timestamp without time zone NOT NULL, - updated_at timestamp without time zone NOT NULL + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL ); @@ -273,6 +259,7 @@ CREATE TABLE public.assignments ( -- CREATE SEQUENCE public.assignments_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -309,6 +296,7 @@ CREATE TABLE public.authentication_tokens ( -- CREATE SEQUENCE public.authentication_tokens_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -364,6 +352,7 @@ CREATE TABLE public.chapters ( -- CREATE SEQUENCE public.chapters_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -395,7 +384,7 @@ CREATE TABLE public.deleted_lessons ( -- CREATE TABLE public.enrollments ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, student_id bigint NOT NULL, group_id bigint NOT NULL, active_since date NOT NULL, @@ -425,6 +414,7 @@ CREATE TABLE public.grade_descriptors ( -- CREATE SEQUENCE public.grade_descriptors_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -461,6 +451,7 @@ CREATE TABLE public.grades ( -- CREATE SEQUENCE public.grades_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -534,6 +525,7 @@ CREATE TABLE public.groups ( -- CREATE SEQUENCE public.groups_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -562,27 +554,6 @@ SELECT NULL::integer AS subject_id; --- --- Name: lesson_table_rows; Type: VIEW; Schema: public; Owner: - --- - -CREATE VIEW public.lesson_table_rows AS -SELECT - NULL::integer AS group_id, - NULL::date AS date, - NULL::timestamp without time zone AS created_at, - NULL::timestamp without time zone AS updated_at, - NULL::integer AS subject_id, - NULL::timestamp without time zone AS deleted_at, - NULL::uuid AS id, - NULL::character varying AS group_name, - NULL::character varying AS chapter_name, - NULL::character varying AS subject_name, - NULL::bigint AS group_student_count, - NULL::bigint AS graded_student_count, - NULL::numeric AS average_mark; - - -- -- Name: lessons; Type: TABLE; Schema: public; Owner: - -- @@ -674,8 +645,8 @@ CREATE TABLE public.roles ( name character varying, resource_type character varying, resource_id integer, - created_at timestamp without time zone, - updated_at timestamp without time zone + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL ); @@ -804,6 +775,7 @@ CREATE TABLE public.organizations ( -- CREATE SEQUENCE public.organizations_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -840,6 +812,7 @@ SELECT -- CREATE SEQUENCE public.roles_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -883,6 +856,7 @@ CREATE TABLE public.skills ( -- CREATE SEQUENCE public.skills_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -946,6 +920,7 @@ CREATE TABLE public.student_images ( -- CREATE SEQUENCE public.student_images_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1104,6 +1079,7 @@ CREATE TABLE public.student_tags ( -- CREATE SEQUENCE public.students_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1138,6 +1114,7 @@ SELECT -- CREATE SEQUENCE public.subjects_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1157,7 +1134,7 @@ ALTER SEQUENCE public.subjects_id_seq OWNED BY public.subjects.id; -- CREATE TABLE public.tags ( - id uuid DEFAULT public.gen_random_uuid() NOT NULL, + id uuid DEFAULT gen_random_uuid() NOT NULL, tag_name character varying NOT NULL, organization_id bigint NOT NULL, shared boolean NOT NULL, @@ -1171,6 +1148,7 @@ CREATE TABLE public.tags ( -- CREATE SEQUENCE public.users_id_seq + AS integer START WITH 1 INCREMENT BY 1 NO MINVALUE @@ -1510,20 +1488,6 @@ CREATE INDEX index_chapters_on_organization_id ON public.chapters USING btree (o CREATE INDEX index_deleted_lessons_on_group_id ON public.deleted_lessons USING btree (group_id); --- --- Name: index_deleted_lessons_on_lesson_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE UNIQUE INDEX index_deleted_lessons_on_lesson_id ON public.deleted_lessons USING btree (lesson_id); - - --- --- Name: index_deleted_lessons_on_subject_id; Type: INDEX; Schema: public; Owner: - --- - -CREATE INDEX index_deleted_lessons_on_subject_id ON public.deleted_lessons USING btree (subject_id); - - -- -- Name: index_enrollments_on_group_dates_and_student; Type: INDEX; Schema: public; Owner: - -- @@ -1601,6 +1565,13 @@ CREATE INDEX index_lessons_on_group_id ON public.lessons USING btree (group_id); CREATE UNIQUE INDEX index_lessons_on_group_id_and_subject_id_and_date ON public.lessons USING btree (group_id, subject_id, date) WHERE (deleted_at IS NULL); +-- +-- Name: index_lessons_on_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_lessons_on_id ON public.lessons USING btree (id); + + -- -- Name: index_lessons_on_subject_id; Type: INDEX; Schema: public; Owner: - -- @@ -1622,6 +1593,13 @@ CREATE INDEX index_roles_on_name ON public.roles USING btree (name); CREATE INDEX index_roles_on_name_and_resource_type_and_resource_id ON public.roles USING btree (name, resource_type, resource_id); +-- +-- Name: index_roles_on_resource_type_and_resource_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_roles_on_resource_type_and_resource_id ON public.roles USING btree (resource_type, resource_id); + + -- -- Name: index_skills_on_organization_id; Type: INDEX; Schema: public; Owner: - -- @@ -1699,6 +1677,20 @@ CREATE INDEX index_tags_on_organization_id ON public.tags USING btree (organizat CREATE UNIQUE INDEX index_users_on_email ON public.users USING btree (email); +-- +-- Name: index_users_roles_on_role_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_users_roles_on_role_id ON public.users_roles USING btree (role_id); + + +-- +-- Name: index_users_roles_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_users_roles_on_user_id ON public.users_roles USING btree (user_id); + + -- -- Name: index_users_roles_on_user_id_and_role_id; Type: INDEX; Schema: public; Owner: - -- @@ -1706,6 +1698,95 @@ CREATE UNIQUE INDEX index_users_on_email ON public.users USING btree (email); CREATE INDEX index_users_roles_on_user_id_and_role_id ON public.users_roles USING btree (user_id, role_id); +-- +-- Name: chapter_summaries _RETURN; Type: RULE; Schema: public; Owner: - +-- + +CREATE OR REPLACE VIEW public.chapter_summaries AS + SELECT c.id, + c.chapter_name, + c.mlid AS chapter_mlid, + o.mlid AS organization_mlid, + concat(o.mlid, '-', c.mlid) AS full_mlid, + c.organization_id, + o.organization_name, + c.deleted_at, + (count(DISTINCT + CASE + WHEN (g.deleted_at IS NULL) THEN g.id + ELSE NULL::integer + END))::integer AS group_count, + (count(DISTINCT + CASE + WHEN ((en.active_since <= CURRENT_DATE) AND ((en.inactive_since IS NULL) OR (en.inactive_since >= CURRENT_DATE)) AND (s.deleted_at IS NULL)) THEN s.id + ELSE NULL::integer + END))::integer AS student_count, + c.created_at, + c.updated_at + FROM ((((public.chapters c + LEFT JOIN public.groups g ON ((g.chapter_id = c.id))) + LEFT JOIN public.enrollments en ON ((en.group_id = g.id))) + LEFT JOIN public.students s ON ((s.id = en.student_id))) + LEFT JOIN public.organizations o ON ((c.organization_id = o.id))) + GROUP BY c.id, o.id; + + +-- +-- Name: group_lesson_summaries _RETURN; Type: RULE; Schema: public; Owner: - +-- + +CREATE OR REPLACE VIEW public.group_lesson_summaries AS + SELECT slu.lesson_id, + slu.lesson_date, + gr.id AS group_id, + gr.chapter_id, + slu.subject_id, + concat(gr.group_name, ' - ', c.chapter_name) AS group_chapter_name, + (round(avg(slu.average_mark), 2))::double precision AS average_mark, + (sum(slu.grade_count))::bigint AS grade_count, + (round((((sum( + CASE + WHEN (slu.grade_count = 0) THEN 0 + ELSE 1 + END))::numeric / (count(slu.*))::numeric) * (100)::numeric), 2))::double precision AS attendance + FROM ((public.student_lesson_summaries slu + JOIN public.groups gr ON ((slu.group_id = gr.id))) + JOIN public.chapters c ON ((gr.chapter_id = c.id))) + WHERE (slu.deleted_at IS NULL) + GROUP BY slu.lesson_id, gr.id, c.id, slu.subject_id, slu.lesson_date + ORDER BY slu.lesson_date; + + +-- +-- Name: group_summaries _RETURN; Type: RULE; Schema: public; Owner: - +-- + +CREATE OR REPLACE VIEW public.group_summaries AS + SELECT g.id, + g.group_name, + g.deleted_at, + g.created_at, + g.chapter_id, + c.chapter_name, + o.id AS organization_id, + o.mlid AS organization_mlid, + c.mlid AS chapter_mlid, + g.mlid, + concat(o.mlid, '-', c.mlid, '-', g.mlid) AS full_mlid, + o.organization_name, + sum( + CASE + WHEN ((en.active_since <= CURRENT_DATE) AND ((en.inactive_since IS NULL) OR (en.inactive_since >= CURRENT_DATE)) AND (s.deleted_at IS NULL)) THEN 1 + ELSE 0 + END) AS student_count + FROM ((((public.groups g + LEFT JOIN public.enrollments en ON ((g.id = en.group_id))) + LEFT JOIN public.students s ON ((s.id = en.student_id))) + LEFT JOIN public.chapters c ON ((g.chapter_id = c.id))) + LEFT JOIN public.organizations o ON ((c.organization_id = o.id))) + GROUP BY g.id, c.id, o.id; + + -- -- Name: lesson_skill_summaries _RETURN; Type: RULE; Schema: public; Owner: - -- @@ -1725,6 +1806,40 @@ CREATE OR REPLACE VIEW public.lesson_skill_summaries AS GROUP BY l.id, sk.id, su.id; +-- +-- Name: organization_summaries _RETURN; Type: RULE; Schema: public; Owner: - +-- + +CREATE OR REPLACE VIEW public.organization_summaries AS + SELECT o.id, + o.organization_name, + o.mlid AS organization_mlid, + (count(DISTINCT + CASE + WHEN ((c.id IS NOT NULL) AND (c.deleted_at IS NULL)) THEN c.id + ELSE NULL::integer + END))::integer AS chapter_count, + (count(DISTINCT + CASE + WHEN ((g.id IS NOT NULL) AND (g.deleted_at IS NULL)) THEN g.id + ELSE NULL::integer + END))::integer AS group_count, + (count(DISTINCT + CASE + WHEN ((s.id IS NOT NULL) AND (s.deleted_at IS NULL)) THEN s.id + ELSE NULL::integer + END))::integer AS student_count, + o.country, + o.updated_at, + o.created_at, + o.deleted_at + FROM (((public.organizations o + LEFT JOIN public.chapters c ON ((c.organization_id = o.id))) + LEFT JOIN public.groups g ON ((g.chapter_id = c.id))) + LEFT JOIN public.students s ON ((s.organization_id = o.id))) + GROUP BY o.id; + + -- -- Name: performance_per_group_per_skill_per_lessons _RETURN; Type: RULE; Schema: public; Owner: - -- @@ -1750,26 +1865,22 @@ CREATE OR REPLACE VIEW public.performance_per_group_per_skill_per_lessons AS -- --- Name: subject_summaries _RETURN; Type: RULE; Schema: public; Owner: - +-- Name: student_analytics_summaries _RETURN; Type: RULE; Schema: public; Owner: - -- -CREATE OR REPLACE VIEW public.subject_summaries AS - SELECT su.id, - su.subject_name, - su.organization_id, - sum( - CASE - WHEN (a.deleted_at IS NOT NULL) THEN 0 - ELSE 1 - END) AS skill_count, - su.created_at, - su.updated_at, - su.deleted_at - FROM ((public.subjects su - LEFT JOIN public.assignments a ON ((su.id = a.subject_id))) - LEFT JOIN public.skills sk ON ((sk.id = a.skill_id))) - WHERE (sk.deleted_at IS NULL) - GROUP BY su.id; +CREATE OR REPLACE VIEW public.student_analytics_summaries AS + SELECT s.id, + s.organization_id, + s.first_name, + s.last_name, + s.old_group_id, + COALESCE(array_agg(en.group_id) FILTER (WHERE (en.group_id IS NOT NULL)), '{}'::bigint[]) AS enrolled_group_ids + FROM ((public.students s + JOIN public.organizations o ON ((s.organization_id = o.id))) + JOIN public.enrollments en ON ((s.id = en.student_id))) + WHERE (s.deleted_at IS NULL) + GROUP BY s.id, s.first_name, s.last_name + ORDER BY s.last_name, s.first_name; -- @@ -1793,30 +1904,32 @@ CREATE OR REPLACE VIEW public.student_averages AS JOIN public.lessons l ON (((l.id = g.lesson_id) AND (l.subject_id = su.id)))) GROUP BY s.id, su.id, sk.skill_name; + -- --- Name: group_lesson_summaries _RETURN; Type: RULE; Schema: public; Owner: - +-- Name: student_lesson_details _RETURN; Type: RULE; Schema: public; Owner: - -- -CREATE OR REPLACE VIEW public.group_lesson_summaries AS - SELECT slu.lesson_id, - slu.lesson_date, - gr.id AS group_id, - gr.chapter_id, - slu.subject_id, - concat(gr.group_name, ' - ', c.chapter_name) AS group_chapter_name, - (round(avg(slu.average_mark), 2))::double precision AS average_mark, - (sum(slu.grade_count))::bigint AS grade_count, - (round((((sum( - CASE - WHEN (slu.grade_count = 0) THEN 0 - ELSE 1 - END))::numeric / (count(slu.*))::numeric) * (100)::numeric), 2))::double precision AS attendance - FROM ((public.student_lesson_summaries slu - JOIN public.groups gr ON ((slu.group_id = gr.id))) - JOIN public.chapters c ON ((gr.chapter_id = c.id))) - WHERE (slu.deleted_at IS NULL) - GROUP BY slu.lesson_id, gr.id, c.id, slu.subject_id, slu.lesson_date - ORDER BY slu.lesson_date; +CREATE OR REPLACE VIEW public.student_lesson_details AS + SELECT s.id AS student_id, + s.first_name, + s.last_name, + s.deleted_at AS student_deleted_at, + l.id AS lesson_id, + l.date, + l.deleted_at AS lesson_deleted_at, + l.subject_id, + round(avg(g.mark), 2) AS average_mark, + count(g.mark) AS grade_count, + COALESCE(jsonb_object_agg(g.skill_id, jsonb_build_object('mark', g.mark, 'grade_descriptor_id', g.grade_descriptor_id, 'skill_name', sk.skill_name)) FILTER (WHERE (sk.skill_name IS NOT NULL)), '{}'::jsonb) AS skill_marks + FROM ((((public.students s + JOIN public.enrollments en ON ((s.id = en.student_id))) + JOIN public.lessons l ON ((en.group_id = l.group_id))) + LEFT JOIN public.grades g ON (((g.student_id = s.id) AND (g.lesson_id = l.id) AND (g.deleted_at IS NULL)))) + LEFT JOIN public.skills sk ON ((sk.id = g.skill_id))) + WHERE ((en.active_since <= l.date) AND ((en.inactive_since IS NULL) OR (en.inactive_since >= l.date))) + GROUP BY s.id, l.id + ORDER BY l.subject_id; + -- -- Name: student_lesson_summaries _RETURN; Type: RULE; Schema: public; Owner: - @@ -1873,146 +1986,26 @@ CREATE OR REPLACE VIEW public.student_tag_table_rows AS -- --- Name: group_summaries _RETURN; Type: RULE; Schema: public; Owner: - +-- Name: subject_summaries _RETURN; Type: RULE; Schema: public; Owner: - -- -CREATE OR REPLACE VIEW public.group_summaries AS - SELECT g.id, - g.group_name, - g.deleted_at, - g.created_at, - g.chapter_id, - c.chapter_name, - o.id AS organization_id, - o.mlid AS organization_mlid, - c.mlid AS chapter_mlid, - g.mlid, - concat(o.mlid, '-', c.mlid, '-', g.mlid) AS full_mlid, - o.organization_name, +CREATE OR REPLACE VIEW public.subject_summaries AS + SELECT su.id, + su.subject_name, + su.organization_id, sum( CASE - WHEN ((en.active_since <= CURRENT_DATE) AND ((en.inactive_since IS NULL) OR (en.inactive_since >= CURRENT_DATE)) AND (s.deleted_at IS NULL)) THEN 1 - ELSE 0 - END) AS student_count - FROM ((((public.groups g - LEFT JOIN public.enrollments en ON ((g.id = en.group_id))) - LEFT JOIN public.students s ON ((s.id = en.student_id))) - LEFT JOIN public.chapters c ON ((g.chapter_id = c.id))) - LEFT JOIN public.organizations o ON ((c.organization_id = o.id))) - GROUP BY g.id, c.id, o.id; - - --- --- Name: student_lesson_details _RETURN; Type: RULE; Schema: public; Owner: - --- - -CREATE OR REPLACE VIEW public.student_lesson_details AS - SELECT s.id AS student_id, - s.first_name, - s.last_name, - s.deleted_at AS student_deleted_at, - l.id AS lesson_id, - l.date, - l.deleted_at AS lesson_deleted_at, - l.subject_id, - round(avg(g.mark), 2) AS average_mark, - count(g.mark) AS grade_count, - COALESCE(jsonb_object_agg(g.skill_id, jsonb_build_object('mark', g.mark, 'grade_descriptor_id', g.grade_descriptor_id, 'skill_name', sk.skill_name)) FILTER (WHERE (sk.skill_name IS NOT NULL)), '{}'::jsonb) AS skill_marks - FROM ((((public.students s - JOIN public.enrollments en ON ((s.id = en.student_id))) - JOIN public.lessons l ON ((en.group_id = l.group_id))) - LEFT JOIN public.grades g ON (((g.student_id = s.id) AND (g.lesson_id = l.id) AND (g.deleted_at IS NULL)))) - LEFT JOIN public.skills sk ON ((sk.id = g.skill_id))) - WHERE ((en.active_since <= l.date) AND ((en.inactive_since IS NULL) OR (en.inactive_since >= l.date))) - GROUP BY s.id, l.id - ORDER BY l.subject_id; - - --- --- Name: student_analytics_summaries _RETURN; Type: RULE; Schema: public; Owner: - --- - -CREATE OR REPLACE VIEW public.student_analytics_summaries AS - SELECT s.id, - s.organization_id, - s.first_name, - s.last_name, - s.old_group_id, - COALESCE(array_agg(en.group_id) FILTER (WHERE (en.group_id IS NOT NULL)), '{}'::bigint[]) AS enrolled_group_ids - FROM ((public.students s - JOIN public.organizations o ON ((s.organization_id = o.id))) - JOIN public.enrollments en ON ((s.id = en.student_id))) - WHERE (s.deleted_at IS NULL) - GROUP BY s.id, s.first_name, s.last_name - ORDER BY s.last_name, s.first_name; - - --- --- Name: chapter_summaries _RETURN; Type: RULE; Schema: public; Owner: - --- - -CREATE OR REPLACE VIEW public.chapter_summaries AS - SELECT c.id, - c.chapter_name, - c.mlid AS chapter_mlid, - o.mlid AS organization_mlid, - concat(o.mlid, '-', c.mlid) AS full_mlid, - c.organization_id, - o.organization_name, - c.deleted_at, - (count(DISTINCT - CASE - WHEN (g.deleted_at IS NULL) THEN g.id - ELSE NULL::integer - END))::integer AS group_count, - (count(DISTINCT - CASE - WHEN ((en.active_since <= CURRENT_DATE) AND ((en.inactive_since IS NULL) OR (en.inactive_since >= CURRENT_DATE)) AND (s.deleted_at IS NULL)) THEN s.id - ELSE NULL::integer - END))::integer AS student_count, - c.created_at, - c.updated_at - FROM ((((public.chapters c - LEFT JOIN public.groups g ON ((g.chapter_id = c.id))) - LEFT JOIN public.enrollments en ON ((en.group_id = g.id))) - LEFT JOIN public.students s ON ((s.id = en.student_id))) - LEFT JOIN public.organizations o ON ((c.organization_id = o.id))) - GROUP BY c.id, o.id; - --- --- Name: organization_summaries _RETURN; Type: RULE; Schema: public; Owner: - --- - -CREATE OR REPLACE VIEW public.organization_summaries AS - SELECT o.id, - o.organization_name, - o.mlid AS organization_mlid, - (count(DISTINCT - CASE - WHEN ((c.id IS NOT NULL) AND (c.deleted_at IS NULL)) THEN c.id - ELSE NULL::integer - END))::integer AS chapter_count, - (count(DISTINCT - CASE - WHEN ((g.id IS NOT NULL) AND (g.deleted_at IS NULL)) THEN g.id - ELSE NULL::integer - END))::integer AS group_count, - (count(DISTINCT - CASE - WHEN ((c.id IS NOT NULL) AND (c.deleted_at IS NULL)) THEN c.student_count - ELSE 0 - WHEN ((s.id IS NOT NULL) AND (s.deleted_at IS NULL)) THEN s.id - ELSE NULL::integer - END))::integer AS student_count, - o.country, - o.updated_at, - o.created_at, - o.deleted_at - FROM (((public.organizations o - LEFT JOIN public.chapters c ON ((c.organization_id = o.id))) - LEFT JOIN public.groups g ON ((g.chapter_id = c.id))) - LEFT JOIN public.students s ON ((s.organization_id = o.id))) - GROUP BY o.id; + WHEN (a.deleted_at IS NOT NULL) THEN 0 + ELSE 1 + END) AS skill_count, + su.created_at, + su.updated_at, + su.deleted_at + FROM ((public.subjects su + LEFT JOIN public.assignments a ON ((su.id = a.subject_id))) + LEFT JOIN public.skills sk ON ((sk.id = a.skill_id))) + WHERE (sk.deleted_at IS NULL) + GROUP BY su.id; -- diff --git a/spec/components/table_components/table_spec.rb b/spec/components/table_components/table_spec.rb new file mode 100644 index 000000000..93cf3f2fb --- /dev/null +++ b/spec/components/table_components/table_spec.rb @@ -0,0 +1,43 @@ +require 'rails_helper' + +# Minimal stub to test TableComponents::Table in isolation without policy concerns +class StubTableRow < ViewComponent::Base + with_collection_parameter :item + + def self.columns(**_) + [{ column_name: 'Name' }] + end + + def initialize(item:, item_counter:, pagy:) + @item = item + end + + erb_template '
<%= @item %>
' +end + +RSpec.describe TableComponents::Table, type: :component do + let(:row_component) { StubTableRow } + + context 'when rows is empty' do + it 'renders the empty message when the empty_message option is provided' do + render_inline(described_class.new(rows: [], row_component: row_component, options: { empty_message: 'No items found' })) + + expect(page).to have_text('No items found') + end + + it 'does not render an empty message when no empty_message option is provided' do + render_inline(described_class.new(rows: [], row_component: row_component)) + + expect(page).not_to have_css('[style*="grid-column: 1/-1"]') + end + end + + context 'when rows is not empty' do + it 'does not render the empty message' do + render_inline(described_class.new(rows: ['Alice'], row_component: row_component, options: { empty_message: 'No items found' })) + + expect(page).not_to have_text('No items found') + expect(page).to have_css('.stub-row', text: 'Alice') + end + end +end From e58173222cf930b9aa08e578572d5e11648d1856 Mon Sep 17 00:00:00 2001 From: Kevin Rukundo Date: Thu, 25 Jun 2026 11:54:13 +0200 Subject: [PATCH 2/4] add linux Tailwind binary --- Gemfile.lock | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index 217a6961d..9528fe480 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -176,6 +176,7 @@ GEM fastimage (2.2.4) ffi (1.17.4) ffi (1.17.4-x64-mingw-ucrt) + ffi (1.17.4-x86_64-linux-gnu) fog-aws (3.33.2) base64 (>= 0.2, < 0.4) fog-core (~> 2.6) @@ -202,6 +203,9 @@ GEM google-protobuf (4.33.4-x64-mingw-ucrt) bigdecimal rake (>= 13) + google-protobuf (4.33.4-x86_64-linux-gnu) + bigdecimal + rake (>= 13) has_scope (0.9.0) actionpack (>= 7.0) activesupport (>= 7.0) @@ -285,6 +289,8 @@ GEM racc (~> 1.4) nokogiri (1.19.3-x64-mingw-ucrt) racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) oauth2 (2.0.18) faraday (>= 0.17.3, < 4.0) jwt (>= 1.0, < 4.0) @@ -320,6 +326,7 @@ GEM racc pg (1.6.3) pg (1.6.3-x64-mingw-ucrt) + pg (1.6.3-x86_64-linux) pg_query (6.2.2) google-protobuf (>= 3.25.3) pg_search (2.3.7) @@ -506,6 +513,7 @@ GEM tailwindcss-ruby (~> 4.0) tailwindcss-ruby (4.1.16) tailwindcss-ruby (4.1.16-x64-mingw-ucrt) + tailwindcss-ruby (4.1.16-x86_64-linux-gnu) thor (1.5.0) tiddle (1.8.1) activerecord (>= 6.1.0) @@ -550,6 +558,7 @@ GEM PLATFORMS ruby x64-mingw-ucrt + x86_64-linux DEPENDENCIES SyslogLogger From 90a89e9bb29f64775e2396d4c2a74f320ab3fc62 Mon Sep 17 00:00:00 2001 From: Kevin Rukundo Date: Thu, 25 Jun 2026 14:21:12 +0200 Subject: [PATCH 3/4] fix test cases for returning empty message when table results are not found --- Gemfile | 5 ++++- config/database.yml | 3 +-- spec/components/table_components/table_spec.rb | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 80d7eb598..05d1893be 100644 --- a/Gemfile +++ b/Gemfile @@ -68,11 +68,14 @@ group :development, :test do gem 'webmock' end +group :development, :test do + gem 'dotenv-rails' +end + group :development do gem 'annotate' gem 'better_errors' gem 'binding_of_caller' - gem 'dotenv-rails' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'pry' diff --git a/config/database.yml b/config/database.yml index cb77f735d..ff86a8993 100644 --- a/config/database.yml +++ b/config/database.yml @@ -3,8 +3,7 @@ default: &default pool: 5 timeout: 5000 host: <%= ENV['DATABASE_HOST'] || 'localhost' %> - # port: 5433 - port: 5432 + port: <%= ENV['DATABASE_PORT'] || 5432 %> username: <%= ENV['DATABASE_USER'] || 'tracker' %> password: <%= ENV['DATABASE_PASSWORD'] || 'tracker' %> diff --git a/spec/components/table_components/table_spec.rb b/spec/components/table_components/table_spec.rb index 93cf3f2fb..2f52d85c0 100644 --- a/spec/components/table_components/table_spec.rb +++ b/spec/components/table_components/table_spec.rb @@ -8,8 +8,9 @@ def self.columns(**_) [{ column_name: 'Name' }] end - def initialize(item:, item_counter:, pagy:) + def initialize(item:, **_) @item = item + super() end erb_template '
<%= @item %>
' From 485603a305ffa8d2a9dcc7af8e623661713ab327 Mon Sep 17 00:00:00 2001 From: Kevin Rukundo Date: Thu, 25 Jun 2026 14:30:09 +0200 Subject: [PATCH 4/4] fix linting errors on double inclusion of groups --- Gemfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 05d1893be..9f349299c 100644 --- a/Gemfile +++ b/Gemfile @@ -46,6 +46,7 @@ end group :development, :test do gem 'bullet', '!= 6.0.0' # 6.0.0 seems to break with Turbolinks + gem 'dotenv-rails' gem 'rails-controller-testing' gem 'rubocop', require: false gem 'rubocop-performance' @@ -68,10 +69,6 @@ group :development, :test do gem 'webmock' end -group :development, :test do - gem 'dotenv-rails' -end - group :development do gem 'annotate' gem 'better_errors'