From 4f5b9dcf890f54524324f82bee06fc93046ec3bf Mon Sep 17 00:00:00 2001 From: Artemiy Solopov Date: Sat, 12 Jul 2025 17:50:56 +0300 Subject: [PATCH] Tasks display --- .rubocop.yml | 8 +++++++- Gemfile | 1 + Gemfile.lock | 5 +++++ app/controllers/tasks_controller.rb | 24 ++++++++++++++++++++++++ app/helpers/tasks_helper.rb | 2 ++ app/models/project.rb | 28 ++++++++++++++++++++++++++-- app/models/task.rb | 20 ++++++++++++++++++++ app/views/projects/index.html.slim | 6 +++++- app/views/tasks/index.html.slim | 14 ++++++++++++++ app/views/tasks/show.html.slim | 5 +++++ config/application.rb | 7 ++++++- config/routes.rb | 7 +++++-- 12 files changed, 120 insertions(+), 7 deletions(-) create mode 100644 app/controllers/tasks_controller.rb create mode 100644 app/helpers/tasks_helper.rb create mode 100644 app/views/tasks/index.html.slim create mode 100644 app/views/tasks/show.html.slim diff --git a/.rubocop.yml b/.rubocop.yml index ee6f28f..1e980ff 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,7 +17,9 @@ AllCops: Style/StringLiterals: Exclude: - - Gemfile # Mostly because of auto-generated files + # Mostly because of auto-generated files + - Gemfile + - config/application.rb Bundler/OrderedGems: Enabled: false @@ -28,6 +30,10 @@ Style/Documentation: Rails/ActionOrder: Enabled: false +Rails/DynamicFindBy: + AllowedMethods: + - find_by_full_number_or_id! + Metrics/AbcSize: Exclude: - db/migrate/*.rb diff --git a/Gemfile b/Gemfile index 911e1b7..2f39ae1 100644 --- a/Gemfile +++ b/Gemfile @@ -40,6 +40,7 @@ gem "thruster", require: false gem "image_processing", "~> 1.2" gem 'slim' +gem 'slim-rails' group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem diff --git a/Gemfile.lock b/Gemfile.lock index 7bb4195..a79f036 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -263,6 +263,10 @@ GEM slim (5.2.1) temple (~> 0.10.0) tilt (>= 2.1.0) + slim-rails (3.7.0) + actionpack (>= 3.1) + railties (>= 3.1) + slim (>= 3.0, < 6.0, != 5.0.0) solid_cable (3.0.8) actioncable (>= 7.2) activejob (>= 7.2) @@ -338,6 +342,7 @@ DEPENDENCIES rubocop rubocop-rails slim + slim-rails solid_cable solid_cache solid_queue diff --git a/app/controllers/tasks_controller.rb b/app/controllers/tasks_controller.rb new file mode 100644 index 0000000..de94a48 --- /dev/null +++ b/app/controllers/tasks_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class TasksController < ApplicationController + before_action :fetch_task, only: %w[show edit update delete] + + def index + if params[:project] + @project = Project.find_by!(code: params[:project]) + @tasks = @project.tasks + else + @tasks = Task.all + end + + @tasks = @tasks.includes(:project) + end + + def show; end + + private + + def fetch_task + @task = Task.find_by_full_number_or_id!(params[:id]) + end +end diff --git a/app/helpers/tasks_helper.rb b/app/helpers/tasks_helper.rb new file mode 100644 index 0000000..ce894d0 --- /dev/null +++ b/app/helpers/tasks_helper.rb @@ -0,0 +1,2 @@ +module TasksHelper +end diff --git a/app/models/project.rb b/app/models/project.rb index dcf7328..9faf29f 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -2,17 +2,41 @@ class Project < ApplicationRecord validates :name, :code, presence: true - validates :code, exclusion: { in: %w[new] }, uniqueness: true + validates :code, exclusion: { in: %w[new] }, uniqueness: true, format: { with: /\A[a-z]{2,}\z/ } has_many :tasks, dependent: :restrict_with_exception has_rich_text :description - normalizes :code, with: ->(code) { code.strip.downcase } + normalizes :code, with: ->(code) { code.strip.downcase.gsub(/\W+/, '') } + + after_commit :create_tasks_number_sequence, on: :create + after_destroy_commit :drop_tasks_number_sequence def to_param return unless id code end + + def tasks_number_sequence_name + "_seq_projects__#{code}_tasks_number" + end + + def next_task_number + result = self.class.connection.exec_query "SELECT NEXT VALUE FOR #{tasks_number_sequence_name} AS task_number" + raise "Expected one result, got #{result.count}" unless result.one? + + result.first['task_number'] + end + + private + + def create_tasks_number_sequence + self.class.connection.execute "CREATE SEQUENCE IF NOT EXISTS #{tasks_number_sequence_name} AS INT UNSIGNED" + end + + def drop_tasks_number_sequence + self.class.connection.execute "DROP SEQUENCE IF EXISTS #{tasks_number_sequence_name}" + end end diff --git a/app/models/task.rb b/app/models/task.rb index a09b0fa..deefbb9 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Task < ApplicationRecord belongs_to :project @@ -5,4 +7,22 @@ class Task < ApplicationRecord validates :number, numericality: { greater_than: 0 } has_rich_text :description + + def to_param + return full_number if association(:project).loaded? + + super + end + + def full_number + "#{project.code.upcase}-#{number}" + end + + def self.find_by_full_number_or_id!(number_or_id) + return find!(number_or_id) if number_or_id.is_a?(Numeric) || number_or_id =~ /\A\d+\z/ + + project_code, number = number_or_id.split('-') + project = Project.find_by!(code: project_code.downcase) + find_by!(project:, number:) + end end diff --git a/app/views/projects/index.html.slim b/app/views/projects/index.html.slim index 1381e4b..c2f0720 100644 --- a/app/views/projects/index.html.slim +++ b/app/views/projects/index.html.slim @@ -1,3 +1,7 @@ +.row + = link_to 'New', new_project_path + ul - @projects.each do |project| - li= link_to project.name, project + li + = link_to project.name, project diff --git a/app/views/tasks/index.html.slim b/app/views/tasks/index.html.slim new file mode 100644 index 0000000..307ceed --- /dev/null +++ b/app/views/tasks/index.html.slim @@ -0,0 +1,14 @@ +.row + = link_to 'New', new_task_path(project: @project&.code) + +- if @tasks.exists? + table + thead + tbody + - @tasks.each do |task| + - cache task do + tr + td= link_to task.full_number, task_path(task) + td= task.title +- else + p No tasks diff --git a/app/views/tasks/show.html.slim b/app/views/tasks/show.html.slim new file mode 100644 index 0000000..672dcf5 --- /dev/null +++ b/app/views/tasks/show.html.slim @@ -0,0 +1,5 @@ +div + em= @task.full_number +h1= @task.title + +section= @task.description diff --git a/config/application.rb b/config/application.rb index 8c2388b..ca32e8b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "boot" require "rails" @@ -37,6 +39,9 @@ module SubtleStorm # config.eager_load_paths << Rails.root.join("extras") # Don't generate system test files. - config.generators.system_tests = nil + config.generators do |g| + g.system_tests nil + g.template_engine :slim + end end end diff --git a/config/routes.rb b/config/routes.rb index 7b4b368..ac03d36 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.routes.draw do resource :session resources :passwords, param: :token @@ -5,7 +7,7 @@ Rails.application.routes.draw do # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. - get "up" => "rails/health#show", as: :rails_health_check + get 'up' => 'rails/health#show', as: :rails_health_check # Render dynamic PWA files from app/views/pwa/* (remember to link manifest in application.html.erb) # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest @@ -13,6 +15,7 @@ Rails.application.routes.draw do # Defines the root path route ("/") # root "posts#index" - + resources :projects + resources :tasks end