diff --git a/.overcommit.yml b/.overcommit.yml new file mode 100644 index 0000000..5b4dd02 --- /dev/null +++ b/.overcommit.yml @@ -0,0 +1,20 @@ +verify_signatures: false +PreCommit: + RuboCop: + enabled: true + on_warn: fail + + BundlerAudit: + enabled: true + command: [ 'bundle', 'audit', 'check', '--update' ] + on_warn: fail + + Brakeman: + enabled: true + command: [ 'brakeman' ] + on_warn: fail + + RSpec: + enabled: true + command: [ 'bundle', 'exec', 'rspec' ] + on_warn: fail \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml index f39cdeb..b81c14b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,5 +4,28 @@ AllCops: Layout/LineLength: Max: 150 -Style/FrozenStringLiteralComment: - Enabled: false \ No newline at end of file +Style/Documentation: + Enabled: false + +Style/NumericLiterals: + Exclude: + - 'test/dummy/db/schema.rb' + +Metrics/BlockLength: + Exclude: + - 'test/dummy/db/schema.rb' + - 'spec/requests/activejob_web/jobs_spec.rb' + - 'spec/requests/activejob_web/job_executions_spec.rb' + - 'spec/requests/activejob_web/job_approval_requests_spec.rb' + - 'test/dummy/db/seeds.rb' + +Metrics/MethodLength: + Exclude: + - 'db/migrate/20231024051840_create_jobs.rb' + - 'test/dummy/db/migrate/20231118052244_create_users.rb' + - 'db/migrate/20231030145254_create_active_storage_tables.active_storage.rb' + - 'db/migrate/20231106105039_create_activejob_web_job_executions.rb' + +Metrics/AbcSize: + Exclude: + - 'db/migrate/20231030145254_create_active_storage_tables.active_storage.rb' diff --git a/Gemfile b/Gemfile index b9e7252..a149efc 100644 --- a/Gemfile +++ b/Gemfile @@ -1,18 +1,32 @@ +# frozen_string_literal: true + source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Specify your gem's dependencies in activejob-web.gemspec. + gemspec gem 'puma' gem 'pg' +gem 'image_processing', '>= 1.2' + gem 'sprockets-rails' +group :development do + gem 'brakeman' + gem 'bundler-audit', require: false + gem 'overcommit', '~> 0.60.0' +end + group :development, :test do + gem 'factory_bot_rails' + gem 'rspec-rails' gem 'rubocop', require: false end +gem 'rails-controller-testing', '~> 1.0', '>= 1.0.5' # Start debugger with binding.b [https://github.com/ruby/debug] # gem "debug", ">= 1.0.0" diff --git a/Gemfile.lock b/Gemfile.lock index f549c0e..50e4066 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,70 +7,71 @@ PATH GEM remote: https://rubygems.org/ specs: - actioncable (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) + actioncable (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) nio4r (~> 2.0) websocket-driver (>= 0.6.1) zeitwerk (~> 2.6) - actionmailbox (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + actionmailbox (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.1.1) - actionpack (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activesupport (= 7.1.1) + actionmailer (7.1.2) + actionpack (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activesupport (= 7.1.2) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.2) - actionpack (7.1.1) - actionview (= 7.1.1) - activesupport (= 7.1.1) + actionpack (7.1.2) + actionview (= 7.1.2) + activesupport (= 7.1.2) nokogiri (>= 1.8.5) + racc rack (>= 2.2.4) rack-session (>= 1.0.1) rack-test (>= 0.6.3) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - actiontext (7.1.1) - actionpack (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + actiontext (7.1.2) + actionpack (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.1.1) - activesupport (= 7.1.1) + actionview (7.1.2) + activesupport (= 7.1.2) builder (~> 3.1) erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - activejob (7.1.1) - activesupport (= 7.1.1) + activejob (7.1.2) + activesupport (= 7.1.2) globalid (>= 0.3.6) - activemodel (7.1.1) - activesupport (= 7.1.1) - activerecord (7.1.1) - activemodel (= 7.1.1) - activesupport (= 7.1.1) + activemodel (7.1.2) + activesupport (= 7.1.2) + activerecord (7.1.2) + activemodel (= 7.1.2) + activesupport (= 7.1.2) timeout (>= 0.4.0) - activestorage (7.1.1) - actionpack (= 7.1.1) - activejob (= 7.1.1) - activerecord (= 7.1.1) - activesupport (= 7.1.1) + activestorage (7.1.2) + actionpack (= 7.1.2) + activejob (= 7.1.2) + activerecord (= 7.1.2) + activesupport (= 7.1.2) marcel (~> 1.0) - activesupport (7.1.1) + activesupport (7.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -81,27 +82,43 @@ GEM mutex_m tzinfo (~> 2.0) ast (2.4.2) - base64 (0.1.1) + base64 (0.2.0) bigdecimal (3.1.4) + brakeman (6.0.1) builder (3.2.4) + bundler-audit (0.9.1) + bundler (>= 1.2.0, < 3) + thor (~> 1.0) + childprocess (4.1.0) concurrent-ruby (1.2.2) connection_pool (2.4.1) crass (1.0.6) - date (3.3.3) - drb (2.1.1) + date (3.3.4) + diff-lcs (1.5.0) + drb (2.2.0) ruby2_keywords erubi (1.12.0) + factory_bot (6.2.1) + activesupport (>= 5.0.0) + factory_bot_rails (6.2.0) + factory_bot (~> 6.2.0) + railties (>= 5.0.0) + ffi (1.16.3) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.1) concurrent-ruby (~> 1.0) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) + iniparse (1.5.0) io-console (0.6.0) - irb (1.8.3) + irb (1.9.0) rdoc reline (>= 0.3.8) json (2.6.3) language_server-protocol (3.17.0.3) - loofah (2.21.4) + loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -110,21 +127,26 @@ GEM net-pop net-smtp marcel (1.0.2) + mini_magick (4.12.0) mini_mime (1.1.5) minitest (5.20.0) - mutex_m (0.1.2) - net-imap (0.4.2) + mutex_m (0.2.0) + net-imap (0.4.5) date net-protocol net-pop (0.1.2) net-protocol - net-protocol (0.2.1) + net-protocol (0.2.2) timeout net-smtp (0.4.0) net-protocol - nio4r (2.5.9) + nio4r (2.6.0) nokogiri (1.15.4-x86_64-darwin) racc (~> 1.4) + overcommit (0.60.0) + childprocess (>= 0.6.3, < 5) + iniparse (~> 1.4) + rexml (~> 3.2) parallel (1.23.0) parser (3.2.2.4) ast (~> 2.4.1) @@ -134,7 +156,7 @@ GEM stringio puma (6.4.0) nio4r (~> 2.0) - racc (1.7.1) + racc (1.7.3) rack (3.0.8) rack-session (2.0.0) rack (>= 3.0.0) @@ -143,20 +165,24 @@ GEM rackup (2.1.0) rack (>= 3) webrick (~> 1.8) - rails (7.1.1) - actioncable (= 7.1.1) - actionmailbox (= 7.1.1) - actionmailer (= 7.1.1) - actionpack (= 7.1.1) - actiontext (= 7.1.1) - actionview (= 7.1.1) - activejob (= 7.1.1) - activemodel (= 7.1.1) - activerecord (= 7.1.1) - activestorage (= 7.1.1) - activesupport (= 7.1.1) + rails (7.1.2) + actioncable (= 7.1.2) + actionmailbox (= 7.1.2) + actionmailer (= 7.1.2) + actionpack (= 7.1.2) + actiontext (= 7.1.2) + actionview (= 7.1.2) + activejob (= 7.1.2) + activemodel (= 7.1.2) + activerecord (= 7.1.2) + activestorage (= 7.1.2) + activesupport (= 7.1.2) bundler (>= 1.15.0) - railties (= 7.1.1) + railties (= 7.1.2) + rails-controller-testing (1.0.5) + actionpack (>= 5.0.1.rc1) + actionview (>= 5.0.1.rc1) + activesupport (>= 5.0.1.rc1) rails-dom-testing (2.2.0) activesupport (>= 5.0.0) minitest @@ -164,24 +190,40 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) - railties (7.1.1) - actionpack (= 7.1.1) - activesupport (= 7.1.1) + railties (7.1.2) + actionpack (= 7.1.2) + activesupport (= 7.1.2) irb rackup (>= 1.0.0) rake (>= 12.2) thor (~> 1.0, >= 1.2.2) zeitwerk (~> 2.6) rainbow (3.1.1) - rake (13.0.6) - rdoc (6.5.0) + rake (13.1.0) + rdoc (6.6.0) psych (>= 4.0.0) regexp_parser (2.8.2) - reline (0.3.9) + reline (0.4.0) io-console (~> 0.5) rexml (3.2.6) - rubocop (1.57.1) - base64 (~> 0.1.1) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (6.0.3) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.12.1) + rubocop (1.57.2) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -192,9 +234,11 @@ GEM rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) + rubocop-ast (1.30.0) parser (>= 3.2.1.0) ruby-progressbar (1.13.0) + ruby-vips (2.1.4) + ffi (~> 1.12) ruby2_keywords (0.0.5) sprockets (4.2.1) concurrent-ruby (~> 1.0) @@ -203,9 +247,9 @@ GEM actionpack (>= 5.2) activesupport (>= 5.2) sprockets (>= 3.0.0) - stringio (3.0.8) + stringio (3.0.9) thor (1.3.0) - timeout (0.4.0) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (2.5.0) @@ -220,10 +264,17 @@ PLATFORMS DEPENDENCIES activejob-web! + brakeman + bundler-audit + factory_bot_rails + image_processing (>= 1.2) + overcommit (~> 0.60.0) pg puma + rails-controller-testing (~> 1.0, >= 1.0.5) + rspec-rails rubocop sprockets-rails BUNDLED WITH - 2.4.19 + 2.4.21 diff --git a/README.md b/README.md index 9ff0d2d..0edd30c 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,116 @@ Or install it yourself as: $ gem install activejob-web ``` + +## Configuring Activejob Web approvers and executors + +By default, Activejob Web uses the `User` class as the model for job approvers and executors. If you need to customize these classes, you can do so in the initializer file located at `config/initializers/activejob_web.rb`. + +For example, if your model to configure is 'Author', you can set the job approvers and executors classes as follows: + +```ruby +# config/initializers/activejob_web.rb + +Activejob::Web.job_approvers_class = 'Author' +Activejob::Web.job_executors_class = 'Author' +``` +where, `job_approvers_class` and `job_executors_class` were defined as `mattr_accessors` in Activejob Web gem. + +## Configuration for Authentication +The ActivejobWeb Gem integrates with the authentication system used in your Rails application. To set up authentication, create a helper file named `authentication_helper.rb` with the `my_app_current_user` method: + +```ruby +# app/helpers/user_helper.rb + +module AuthenticationHelper + def my_app_current_user + # Specify the current user here + User.find(current_user.id) + end +end +``` +In the `my_app_current_user` method, specify the logic to fetch the current user for authentication. Replace the placeholder `User.find(current_user.id)` with the actual code that retrieves the current user. + +Make sure that your application controller includes the helper file named `ActivejobWeb::JobsHelper` and **helper_method** `activejob_web_current_user` as shown below, +if not included please include the helper file and helper_method for the authentication related configurations. + +```ruby +# app/controllers/application_controller.rb + +class ApplicationController < ActionController::Base + include ActivejobWeb::JobsHelper + helper_method :activejob_web_current_user +end +``` + +## Enabling Routes for specific users to edit the Jobs +By enabling this route, The specified super admin users will only have permission to edit the jobs. The implementation involves utilizing `route constraints` and a custom `lambda function` to restrict access based on a `super admin scope`. +Assuming that your model to configure is 'User'. +### 1. Define a scope named `super_admin_users` in the `User` model +Specify the condition in the scope that it should return the users with the permission for the edit page of jobs. The example code was shown below the custom lambda. +### 2. Define a Custom Lambda Method + +In your `User` model or the model you defined for approvers/executors, define a method named `allow_admin_access?` that represents the condition for user access. This lambda function will be used as a route constraint. + +```ruby +# app/models/user.rb + +class User < ApplicationRecord + # Your existing user model code + + # Define a scope for super admins here + scope :super_admin_users, -> { User.all.limit(10) } + + # Your other scopes and methods... + + # Define a custom lambda inside a method for route constraint + def self.allow_admin_access? + lambda do |_request| + super_admin_users.include?(current_user) + # Your custom logic to determine user access goes here + end + end +end +``` +If the condition specified in the `allow_admin_access?` returns `true` the current user can able to edit the jobs. + +### 3. Route customization +In Activejob Web, you have the flexibility to customize routes based on specific user requirements. By default, every user is allowed to edit jobs, but you can customize the routes to be accessible only by specific users. + +For that, first you have to specify the scope and lambda for the `User` or the required model as mentioned in the previous points.Then, start by configuring the `enable_custom_routes` option in the initializer file `activejob_web.rb`. Set it to `true` as shown below: + +```ruby +# config/initializers/activejob_web.rb + +Rails.application.config.enable_custom_routes = true +``` +Your routes for jobs edit will look like this +```ruby +if Rails.application.config.enable_custom_routes == true + get 'activejob_web/jobs/:id/edit', to: 'activejob_web/jobs#edit', + constraints: UserRoleConstraint.new(Activejob::Web.job_approvers_class.constantize.allow_admin_access?), + as: 'edit_activejob_web_job' + else + get 'activejob_web/jobs/:id/edit', to: 'activejob_web/jobs#edit' + end +``` +In this route, you have to customize the method name `allow_admin_access?` if you are using a different name for the method. + +The edit jobs route uses a constraint file named `user_role_constraint.rb` for processing the lambda you define in the `allow_admin_access?` method as shown below, +```ruby +# app/constraints/user_role_constraint.rb + +class UserRoleConstraint + def initialize(lambda_function) + @lambda_function = lambda_function + end + + def matches?(request) + # Call the lambda function and pass the request as an argument + @lambda_function.call(request) + end +end +``` ## Contributing Contribution directions go here. diff --git a/Rakefile b/Rakefile index 34bcc14..5a624a1 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'bundler/setup' APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__) diff --git a/activejob-web.gemspec b/activejob-web.gemspec index 2db4660..cdac4d7 100644 --- a/activejob-web.gemspec +++ b/activejob-web.gemspec @@ -1,27 +1,28 @@ -require_relative 'lib/activejob/web/version' +require_relative "lib/activejob/web/version" Gem::Specification.new do |spec| - spec.name = 'activejob-web' + spec.name = "activejob-web" spec.version = Activejob::Web::VERSION - spec.authors = %w[gowtham mohammednazeer] - spec.email = %w[gowtham.kuppusamy@mallow-tech.com mohammednazeer@mallow-tech.com] - spec.homepage = 'TODO' - spec.summary = 'TODO: Summary of Activejob::Web.' - spec.description = 'TODO: Description of Activejob::Web.' - spec.license = 'MIT' + spec.authors = ["mohammednazeer"] + spec.email = ["mohammednazeer@mallow-tech.com"] + spec.homepage = 'https://example.com' + spec.summary = "Summary of Activejob::Web." + spec.description = "Description of Activejob::Web." + spec.license = "MIT" # Prevent pushing this gem to RubyGems.org. To allow pushes either set the "allowed_push_host" # to allow pushing to a single host or delete this section to allow pushing to any host. - spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'" + spec.metadata["allowed_push_host"] = 'http://mygemserver.com' - spec.metadata['homepage_uri'] = spec.homepage - spec.metadata['source_code_uri'] = "TODO: Put your gem's public repo URL here." - spec.metadata['changelog_uri'] = "TODO: Put your gem's CHANGELOG.md URL here." + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = 'https://example.com' + spec.metadata["changelog_uri"] = 'https://example.com' spec.files = Dir.chdir(File.expand_path(__dir__)) do - Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md'] + Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"] end - spec.required_ruby_version = '>= 3.2.2' - spec.add_dependency 'rails', '>= 7.0.8' + spec.add_dependency "rails", ">= 7.0.8" + spec.add_development_dependency 'rspec-rails' + spec.add_development_dependency "factory_bot_rails" end diff --git a/app/assets/images/activejob/web/sample.png b/app/assets/images/activejob/web/sample.png new file mode 100644 index 0000000..3b5aaf2 Binary files /dev/null and b/app/assets/images/activejob/web/sample.png differ diff --git a/app/constraints/user_role_constraint.rb b/app/constraints/user_role_constraint.rb new file mode 100644 index 0000000..df5731b --- /dev/null +++ b/app/constraints/user_role_constraint.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class UserRoleConstraint + def initialize(lambda_function) + @lambda_function = lambda_function + end + + def matches?(request) + # Call the lambda function and pass the request as an argument + @lambda_function.call(request) + end +end diff --git a/app/controllers/activejob_web/application_controller.rb b/app/controllers/activejob_web/application_controller.rb new file mode 100644 index 0000000..5181c45 --- /dev/null +++ b/app/controllers/activejob_web/application_controller.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module ActivejobWeb + class ApplicationController < ActionController::Base + include ActivejobWeb::JobsHelper + helper_method :activejob_web_current_user + protect_from_forgery with: :exception + end +end diff --git a/app/controllers/activejob_web/job_approval_requests_controller.rb b/app/controllers/activejob_web/job_approval_requests_controller.rb new file mode 100644 index 0000000..39dca6f --- /dev/null +++ b/app/controllers/activejob_web/job_approval_requests_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActivejobWeb + class JobApprovalRequestsController < ApplicationController + before_action :set_job_and_job_execution + before_action :set_approval_request, only: %i[action update] + + def index + @job_approval_requests = ActivejobWeb::JobApprovalRequest.where(job_execution_id: params[:job_execution_id], + response: nil).includes(job_execution: %i[job user]) + end + + def action; end + + def update + return unless @job_approval_request.update(job_approval_request_params) + + redirect_to activejob_web_job_job_execution_job_approval_requests_path(@job, @job_execution), + notice: 'Job approval request updated successfully.' + end + + private + + def set_approval_request + @job_approval_request = ActivejobWeb::JobApprovalRequest.find(params[:id]) + end + + def set_job_and_job_execution + @job = ActivejobWeb::Job.find(params[:job_id]) + @job_execution = ActivejobWeb::JobExecution.find(params[:job_execution_id]) + end + + def job_approval_request_params + params.require(:activejob_web_job_approval_request).permit(:response, :approver_comments) + end + end +end diff --git a/app/controllers/activejob_web/job_executions_controller.rb b/app/controllers/activejob_web/job_executions_controller.rb new file mode 100644 index 0000000..4b62fb3 --- /dev/null +++ b/app/controllers/activejob_web/job_executions_controller.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module ActivejobWeb + class JobExecutionsController < ApplicationController + before_action :set_job_id + def index + @job_executions = ActivejobWeb::JobExecution.where(job_id: params[:job_id]) + @job_execution = @job.job_executions.new + end + + def edit + @job_execution = @job.job_executions.find(params[:id]) + end + + def update + @job_execution = ActivejobWeb::JobExecution.find(params[:id]) + if @job_execution.update(job_execution_params) + redirect_to activejob_web_job_job_execution_path(@job), notice: 'Job execution was successfully updated.' + else + render :edit + end + end + + def show + @job_execution = ActivejobWeb::JobExecution.find(params[:id]) + end + + def create + @job_execution = @job.job_executions.new(job_execution_params) + @job_execution.requestor_id = activejob_web_current_user.id + if @job_execution.save + flash[:notice] = 'Job execution created successfully.' + redirect_to activejob_web_job_job_executions_path(@job) + else + @job_executions = ActivejobWeb::JobExecution.where(job_id: params[:job_id]) + render :index + end + end + end +end + + private + +def job_execution_params + params.require(:activejob_web_job_execution).permit(:requestor_comments, :status, :job_id, :auto_execute_on_approval) +end + +def set_job_id + @job = ActivejobWeb::Job.find(params[:job_id]) +end diff --git a/app/controllers/activejob_web/jobs_controller.rb b/app/controllers/activejob_web/jobs_controller.rb new file mode 100644 index 0000000..5c249d0 --- /dev/null +++ b/app/controllers/activejob_web/jobs_controller.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +module ActivejobWeb + class JobsController < ApplicationController + before_action :set_job, only: %i[show edit update] + + def index + @jobs = ActivejobWeb::Job.includes(:executors).where(activejob_web_job_executors: { executor_id: activejob_web_current_user.id }) + end + + def show + if @job.executors.include?(activejob_web_current_user) + render :show + else + redirect_to root_path + flash[:notice] = 'You are not authorized to perform this action.' + end + end + + def edit; end + + def update + @job.approver_ids = job_params[:approver_ids] + @job.executor_ids = job_params[:executor_ids] + if @job.save + redirect_to activejob_web_job_path(@job) + flash[:notice] = 'Job was successfully updated.' + else + render :edit + end + end + + def download_pdf + if @job.template_file.attached? + send_file @job.template_file.download + else + flash[:error] = 'Template file not found.' + redirect_to @job + end + end + + private + + def set_job + @job = ActivejobWeb::Job.find(params[:id]) + end + + def job_params + params.require(:activejob_web_job).permit(approver_ids: [], executor_ids: []) + end + end +end diff --git a/app/helpers/activejob_web/job_approval_requests_helper.rb b/app/helpers/activejob_web/job_approval_requests_helper.rb new file mode 100644 index 0000000..0db48b0 --- /dev/null +++ b/app/helpers/activejob_web/job_approval_requests_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ActivejobWeb + module JobApprovalRequestsHelper + end +end diff --git a/app/helpers/activejob_web/job_executions_helper.rb b/app/helpers/activejob_web/job_executions_helper.rb new file mode 100644 index 0000000..954a4a8 --- /dev/null +++ b/app/helpers/activejob_web/job_executions_helper.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +module ActivejobWeb + module JobExecutionsHelper + end +end diff --git a/app/helpers/activejob_web/jobs_helper.rb b/app/helpers/activejob_web/jobs_helper.rb new file mode 100644 index 0000000..f4bee23 --- /dev/null +++ b/app/helpers/activejob_web/jobs_helper.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module ActivejobWeb + module JobsHelper + include AuthenticationHelper + + def activejob_web_current_user + my_app_current_user + end + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..d394c3d --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,7 @@ +class ApplicationJob < ActiveJob::Base + # Automatically retry jobs that encountered a deadlock + # retry_on ActiveRecord::Deadlocked + + # Most jobs are safe to ignore if the underlying records are no longer available + # discard_on ActiveJob::DeserializationError +end diff --git a/app/jobs/auto_enqueue_job.rb b/app/jobs/auto_enqueue_job.rb new file mode 100644 index 0000000..6a3b66f --- /dev/null +++ b/app/jobs/auto_enqueue_job.rb @@ -0,0 +1,7 @@ +class AutoEnqueueJob < ApplicationJob + queue_as :default + + def perform(job_id,input_arguments) + # puts "Processing AutoEnqueueJob for JobExecution ID: #{job_execution_id}, Job ID: #{job_id}, Input Arguments: #{input_arguments}" + end +end diff --git a/app/models/activejob_web.rb b/app/models/activejob_web.rb new file mode 100644 index 0000000..3ff9a03 --- /dev/null +++ b/app/models/activejob_web.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module ActivejobWeb + def self.table_name_prefix + 'activejob_web_' + end +end diff --git a/app/models/activejob_web/job.rb b/app/models/activejob_web/job.rb new file mode 100644 index 0000000..ece50db --- /dev/null +++ b/app/models/activejob_web/job.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActivejobWeb + class Job < ApplicationRecord + self.primary_key = 'id' + + # == Associations ================================================================================================== + has_and_belongs_to_many :approvers, class_name: Activejob::Web.job_approvers_class.to_s, join_table: 'activejob_web_job_approvers', + association_foreign_key: 'approver_id' + has_and_belongs_to_many :executors, class_name: Activejob::Web.job_executors_class.to_s, join_table: 'activejob_web_job_executors', + association_foreign_key: 'executor_id' + has_one_attached :template_file + has_many :job_executions + + # == Validations =================================================================================================== + validates :title, presence: true, length: { maximum: 255 } + validates :description, presence: true, length: { maximum: 1000 } + + # == Callbacks ===================================================================================================== + after_initialize :set_default_queue + + private + + # Default value for queue + def set_default_queue + self.queue ||= 'default' # Set your desired default value for the queue attribute + end + end +end diff --git a/app/models/activejob_web/job_approval_request.rb b/app/models/activejob_web/job_approval_request.rb new file mode 100644 index 0000000..a0dbc30 --- /dev/null +++ b/app/models/activejob_web/job_approval_request.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module ActivejobWeb + class JobApprovalRequest < ApplicationRecord + belongs_to :approver, class_name: Activejob::Web.job_approvers_class.to_s, foreign_key: 'approver_id' + belongs_to :job_execution, class_name: 'ActivejobWeb::JobExecution', foreign_key: 'job_execution_id' + after_update :update_job_execution_status + def update_job_execution_status + if self.response == 1 + job_execution.enqueue_job_if_approved + end + end + end +end diff --git a/app/models/activejob_web/job_execution.rb b/app/models/activejob_web/job_execution.rb new file mode 100644 index 0000000..d04be04 --- /dev/null +++ b/app/models/activejob_web/job_execution.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module ActivejobWeb + class JobExecution < ApplicationRecord + enum status: { + requested: 0, + approved: 1, + rejected: 2, + executed: 3, + cancelled: 4, + succeeded: 5, + failed: 6 + } + + validates :requestor_comments, presence: true + + after_initialize :set_default_status + after_save :send_job_approval_request + + belongs_to :job, class_name: 'ActivejobWeb::Job', foreign_key: 'job_id' + has_many :job_approval_requests + belongs_to :user, class_name: 'User', foreign_key: 'requestor_id' + def response_count + job_approval_requests.where(response: 1).count + end + def enqueue_job_if_approved + if auto_execute_on_approval? && enough_approvals? + # needs to enqueue the job with the job id and input arguments + AutoEnqueueJob.perform_later(job.id, job.input_arguments) + end + end + + private + def set_default_status + self.status ||= :requested + end + def send_job_approval_request + job.approvers.each do |approver| + job_approval_requests.create(job_execution_id: id, approver_id: approver.id) + end + end + + def enough_approvals? + response_count >= job.minimum_approvals_required + end + end +end \ No newline at end of file diff --git a/app/views/activejob_web/job_approval_requests/action.html.erb b/app/views/activejob_web/job_approval_requests/action.html.erb new file mode 100644 index 0000000..612a056 --- /dev/null +++ b/app/views/activejob_web/job_approval_requests/action.html.erb @@ -0,0 +1,29 @@ +<%= form_for [@job, @job_execution, @job_approval_request], url: activejob_web_job_job_execution_job_approval_request_path(@job, @job_execution, @job_approval_request) do |form| %> + <% if @job_approval_request.errors.any? %> + <% @job_approval_request.errors.full_messages.each do |message| %> +
<%= message %>
+ <% end %> + <% end %> +Job Title: <%= @job_approval_request.job_execution.job.title %>
+Job Description: <%= @job_approval_request.job_execution.job.description %>
+Minimum Approvals Required: <%= @job_approval_request.job_execution.job.minimum_approvals_required %>
+Priority: <%= @job_approval_request.job_execution.job.priority %>
+Requestor Name: <%= @job_approval_request.job_execution.user.name %>
+Requestor email: <%= @job_approval_request.job_execution.user.email %>
+ +| Job title | +Requestor name | +Actions | +
|---|---|---|
| <%= approval_request.job_execution.job.title %> | +<%= approval_request.job_execution.user.name %> | +<%= button_to 'Approve/Decline', action_activejob_web_job_job_execution_job_approval_request_path(approval_request.job_execution.job, approval_request.job_execution, approval_request), method: :get %> | +
| ID | +Status | +Requestor Comments | +Auto Execute On Approval | +Actions | +|
|---|---|---|---|---|---|
| <%= job_execution.id %> | +<%= job_execution.status %> | +<%= job_execution.requestor_comments %> | +<%= job_execution.auto_execute_on_approval %> | +<%= link_to 'Show', activejob_web_job_job_execution_path(@job,job_execution) %> | +<%= link_to 'Back', activejob_web_jobs_path %> | +
ID: <%= @job_execution.id %>
+Status: <%= @job_execution.status %>
+Requestor Comments: <%= @job_execution.requestor_comments %>
+Auto_execute_on_approval: <%= @job_execution.auto_execute_on_approval %>
+Reason for Failure: <%= @job_execution.reason_for_failure %>
+Arguments:<%= @job_execution.arguments %>
+Auto Execute on Approval: <%= @job_execution.auto_execute_on_approval %>
+Run At: <%= @job_execution.run_at %>
+Execution Started At: <%= @job_execution.execution_started_at %>
+ +<%= link_to 'Edit', edit_activejob_web_job_job_execution_path(@job,@job_execution) %> | +<%= link_to 'Back', activejob_web_job_job_executions_path %> +<%= link_to 'Job_approval_requests',activejob_web_job_job_execution_job_approval_requests_path(@job, @job_execution, @job_approval_requests) %> diff --git a/app/views/activejob_web/jobs/edit.html.erb b/app/views/activejob_web/jobs/edit.html.erb new file mode 100644 index 0000000..d17345f --- /dev/null +++ b/app/views/activejob_web/jobs/edit.html.erb @@ -0,0 +1,47 @@ +<%= message %>
+ <% end %> + <% end %> + +| Title | +Description | +ID | +queue | +Actions | +
|---|---|---|---|---|
| <%= job.title %> | +<%= job.description %> | +<%= job.id %> | +<%= job.queue %> | +<%= link_to 'Show', activejob_web_job_path(job) %> | +
Description: <%= @job.description %>
+Input Arguments: <%= @job.input_arguments %>
+Max Run Time: <%= @job.max_run_time %>
+Minimum Approvals Required: <%= @job.minimum_approvals_required %>
+Priority: <%= @job.priority %>
+Queue: <%= @job.queue %>
+<% if @job.template_file.attached? %> + <%= link_to "Download PDF", rails_blob_path(@job.template_file, disposition: "attachment"), class: "btn btn-primary" %> +<% else %> +no template available
+<% end %> +<% if Activejob::Web.job_approvers_class.constantize.super_admin_users.include?(activejob_web_current_user) %> +<%= link_to 'Edit Job', edit_activejob_web_job_path(@job) %>
+<% end %> +<%= link_to 'Back', activejob_web_jobs_path %>
+ +<%= link_to 'Job Executions', activejob_web_job_job_executions_path(@job, @job_executions) %>
diff --git a/bin/rails b/bin/rails index dd5828f..d8e745b 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + # This command will automatically be run when you run "rails" with Rails gems # installed from the root of your application. diff --git a/config/initializers/activejob_web.rb b/config/initializers/activejob_web.rb new file mode 100644 index 0000000..1ab1b8e --- /dev/null +++ b/config/initializers/activejob_web.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Activejob::Web.job_approvers_class = 'Author' +# Activejob::Web.job_executors_class = 'Author' + +Rails.application.config.enable_custom_routes = false diff --git a/config/routes.rb b/config/routes.rb index 643baa7..a8b205b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,27 @@ +# frozen_string_literal: true + Rails.application.routes.draw do - # Defines the root path route ("/") + root 'activejob_web/jobs#index' + + namespace :activejob_web do + resources :jobs, only: %i[index show update] do + resources :job_executions do + resources :job_approval_requests do + member do + get :action + end + end + end + member do + get :download_pdf + end + end + end + if Rails.application.config.enable_custom_routes == true + get 'activejob_web/jobs/:id/edit', to: 'activejob_web/jobs#edit', + constraints: UserRoleConstraint.new(Activejob::Web.job_approvers_class.constantize.allow_admin_access?), + as: 'edit_activejob_web_job' + else + get 'activejob_web/jobs/:id/edit', to: 'activejob_web/jobs#edit', as: 'edit_activejob_web_job' + end end diff --git a/db/migrate/20231024051840_create_jobs.rb b/db/migrate/20231024051840_create_jobs.rb new file mode 100644 index 0000000..113b771 --- /dev/null +++ b/db/migrate/20231024051840_create_jobs.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateJobs < ActiveRecord::Migration[7.1] + def change + create_table :activejob_web_jobs, id: false do |t| + t.uuid :id, default: -> { 'gen_random_uuid()' }, null: false + t.string :title + t.string :description + t.json :input_arguments + t.integer :max_run_time + t.integer :minimum_approvals_required + t.integer :priority + t.integer :queue + + t.timestamps + end + end +end diff --git a/db/migrate/20231030101923_create_join_table_job_approvers.rb b/db/migrate/20231030101923_create_join_table_job_approvers.rb new file mode 100644 index 0000000..47f2642 --- /dev/null +++ b/db/migrate/20231030101923_create_join_table_job_approvers.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CreateJoinTableJobApprovers < ActiveRecord::Migration[7.1] + def change + create_table :activejob_web_job_approvers, id: false do |t| + t.column :job_id, :uuid + t.column :approver_id, :integer + end + end +end diff --git a/db/migrate/20231030102259_create_join_table_job_executors.rb b/db/migrate/20231030102259_create_join_table_job_executors.rb new file mode 100644 index 0000000..f3da157 --- /dev/null +++ b/db/migrate/20231030102259_create_join_table_job_executors.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class CreateJoinTableJobExecutors < ActiveRecord::Migration[7.1] + def change + create_table :activejob_web_job_executors, id: false do |t| + t.column :job_id, :uuid + t.column :executor_id, :integer + end + end +end diff --git a/db/migrate/20231030145254_create_active_storage_tables.active_storage.rb b/db/migrate/20231030145254_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..4dfc381 --- /dev/null +++ b/db/migrate/20231030145254_create_active_storage_tables.active_storage.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + primary_key_type, foreign_key_type = primary_and_foreign_key_types + unless table_exists?(:active_storage_blobs) + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [:key], unique: true + end + end + # Use Active Record's configured type for primary and foreign keys + + unless table_exists?(:active_storage_attachments) + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index %i[record_type record_id name blob_id], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + return if table_exists?(:active_storage_variant_records) + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index %i[blob_id variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/migrate/20231106105039_create_activejob_web_job_executions.rb b/db/migrate/20231106105039_create_activejob_web_job_executions.rb new file mode 100644 index 0000000..c731f90 --- /dev/null +++ b/db/migrate/20231106105039_create_activejob_web_job_executions.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class CreateActivejobWebJobExecutions < ActiveRecord::Migration[7.1] + def change + create_table :activejob_web_job_executions do |t| + t.integer :requestor_id + t.uuid :job_id + t.string :requestor_comments + t.json :arguments + t.integer :status + t.string :reason_for_failure + t.boolean :auto_execute_on_approval + t.timestamp :run_at + t.timestamp :execution_started_at + t.timestamps + end + end +end diff --git a/db/migrate/20231122042337_create_job_approval_requests.rb b/db/migrate/20231122042337_create_job_approval_requests.rb new file mode 100644 index 0000000..cf7503c --- /dev/null +++ b/db/migrate/20231122042337_create_job_approval_requests.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateJobApprovalRequests < ActiveRecord::Migration[7.1] + def change + create_table :activejob_web_job_approval_requests do |t| + t.integer :job_execution_id + t.integer :approver_id + t.integer :response + t.string :approver_comments + + t.timestamps + end + end +end diff --git a/lib/activejob/web.rb b/lib/activejob/web.rb index cb94080..209c764 100644 --- a/lib/activejob/web.rb +++ b/lib/activejob/web.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + require 'activejob/web/version' require 'activejob/web/engine' module Activejob module Web - # Your code goes here... + mattr_accessor :job_approvers_class, default: 'User' + mattr_accessor :job_executors_class, default: 'User' end end diff --git a/lib/activejob/web/engine.rb b/lib/activejob/web/engine.rb index 6cab492..f08d16a 100644 --- a/lib/activejob/web/engine.rb +++ b/lib/activejob/web/engine.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Activejob module Web class Engine < ::Rails::Engine diff --git a/lib/activejob/web/version.rb b/lib/activejob/web/version.rb index f5c83eb..0eeffe7 100644 --- a/lib/activejob/web/version.rb +++ b/lib/activejob/web/version.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + module Activejob module Web - VERSION = '0.1.0'.freeze + VERSION = '0.1.0' end end diff --git a/lib/tasks/activejob/web_tasks.rake b/lib/tasks/activejob/web_tasks.rake index 0f60b46..1996cc2 100644 --- a/lib/tasks/activejob/web_tasks.rake +++ b/lib/tasks/activejob/web_tasks.rake @@ -1,3 +1,4 @@ +# frozen_string_literal: true # desc "Explaining what the task does" # task :activejob_web do # # Task goes here diff --git a/spec/factories/job_approval_requests.rb b/spec/factories/job_approval_requests.rb new file mode 100644 index 0000000..e74b3af --- /dev/null +++ b/spec/factories/job_approval_requests.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# spec/factories/job_approval_requests.rb +FactoryBot.define do + factory :job_approval_request, class: ActivejobWeb::JobApprovalRequest do + job_execution_id { 1 } + approver_id { 1 } + response { 1 } + approver_comments { 'Comments' } + end +end diff --git a/spec/factories/job_executions.rb b/spec/factories/job_executions.rb new file mode 100644 index 0000000..8cc0e47 --- /dev/null +++ b/spec/factories/job_executions.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# spec/factories/job_executions.rb +FactoryBot.define do + factory :job_execution, class: ActivejobWeb::JobExecution do + requestor_id { 1 } # Adjust as needed + job_id { SecureRandom.uuid } # Generates a random UUID + requestor_comments { 'Requestor comments' } + arguments { { key: 'value' } } # Example JSON structure, adjust as needed + status { 0 } # Example status, adjust as needed + reason_for_failure { 'Reason for failure' } + auto_execute_on_approval { false } + run_at { Time.current } + execution_started_at { Time.current } + end + + factory :valid_job_execution, class: ActivejobWeb::JobExecution do + status { 'requested' } + requestor_comments { 'joy' } + auto_execute_on_approval { true } + end + + factory :invalid_job_execution, class: ActivejobWeb::JobExecution do + # This factory intentionally creates an invalid job execution with an invalid status + status { 'approved' } + auto_execute_on_approval { true } + end +end diff --git a/spec/factories/jobs.rb b/spec/factories/jobs.rb new file mode 100644 index 0000000..b349891 --- /dev/null +++ b/spec/factories/jobs.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :job, class: ActivejobWeb::Job do + title { 'Activejob' } + description { 'Web Gem' } + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 0000000..9cb3c76 --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user, class: User do + email { 'user@gmail.com' } + name { 'user' } + password { 'password@123' } + end + + factory :approver, class: User do + email { 'testapprover@gmail.com' } + name { 'Test approver' } + password { 'password@123' } + end + + factory :approver1, class: User do + email { 'testapprover1@gmail.com' } + name { 'First approver' } + password { 'password@123' } + end + + factory :executor, class: User do + email { 'testexecutor@gmail.com' } + name { 'Test executor' } + password { 'password@123' } + end + + factory :executor1, class: User do + email { 'testexecutor1@gmail.com' } + name { 'First executor' } + password { 'password@123' } + end +end diff --git a/spec/helpers/activejob_web/job_approval_requests_helper_spec.rb b/spec/helpers/activejob_web/job_approval_requests_helper_spec.rb new file mode 100644 index 0000000..8bc88ec --- /dev/null +++ b/spec/helpers/activejob_web/job_approval_requests_helper_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the ActivejobWeb::JobApprovalRequestsHelper. For example: +# +# describe ActivejobWeb::JobApprovalRequestsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe ActivejobWeb::JobApprovalRequestsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/activejob_web/job_executions_helper_spec.rb b/spec/helpers/activejob_web/job_executions_helper_spec.rb new file mode 100644 index 0000000..89215a3 --- /dev/null +++ b/spec/helpers/activejob_web/job_executions_helper_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the ActivejobWeb::JobExecutionsHelper. For example: +# +# describe ActivejobWeb::JobExecutionsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe ActivejobWeb::JobExecutionsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/helpers/activejob_web/jobs_helper_spec.rb b/spec/helpers/activejob_web/jobs_helper_spec.rb new file mode 100644 index 0000000..563ba52 --- /dev/null +++ b/spec/helpers/activejob_web/jobs_helper_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the ActivejobWeb::JobsHelper. For example: +# +# describe ActivejobWeb::JobsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe ActivejobWeb::JobsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/jobs/auto_enqueue_job_spec.rb b/spec/jobs/auto_enqueue_job_spec.rb new file mode 100644 index 0000000..8c29488 --- /dev/null +++ b/spec/jobs/auto_enqueue_job_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AutoEnqueueJob, type: :job do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/activejob_web/job_approval_request_spec.rb b/spec/models/activejob_web/job_approval_request_spec.rb new file mode 100644 index 0000000..3e5abd6 --- /dev/null +++ b/spec/models/activejob_web/job_approval_request_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivejobWeb::JobApprovalRequest, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/activejob_web/job_execution_spec.rb b/spec/models/activejob_web/job_execution_spec.rb new file mode 100644 index 0000000..018becc --- /dev/null +++ b/spec/models/activejob_web/job_execution_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivejobWeb::JobExecution, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/activejob_web/job_spec.rb b/spec/models/activejob_web/job_spec.rb new file mode 100644 index 0000000..b83f934 --- /dev/null +++ b/spec/models/activejob_web/job_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivejobWeb::Job, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..2269bcc --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../test/dummy/config/environment', __dir__) +# Prevent database truncation if the environment is production +abort('The Rails environment is running in production mode!') if Rails.env.production? +require 'rspec/rails' +require 'factory_bot_rails' +FactoryBot.definition_file_paths = [File.expand_path('factories', __dir__)] +FactoryBot.find_definitions +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{Rails.root}/spec/fixtures" + + config.include FactoryBot::Syntax::Methods + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/6-0/rspec-rails + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + + config.include FactoryBot::Syntax::Methods + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/requests/activejob_web/job_approval_requests_spec.rb b/spec/requests/activejob_web/job_approval_requests_spec.rb new file mode 100644 index 0000000..0431479 --- /dev/null +++ b/spec/requests/activejob_web/job_approval_requests_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'ActivejobWeb::JobApprovalRequests', type: :request do + let(:job) { create(:job) } + let(:user) { create(:user) } + let(:job_execution) { create(:job_execution, job_id: job.id, requestor_id: user.id) } + let(:job_approval_request) { create(:job_approval_request, job_execution_id: job_execution.id, approver_id: user.id) } + before do + allow_any_instance_of(ActivejobWeb::JobsHelper).to receive(:activejob_web_current_user).and_return(user) + end + describe 'GET #index' do + it 'renders the index template' do + get activejob_web_job_job_execution_job_approval_requests_path(job.id, job_execution.id) + expect(response).to render_template('index') + expect(response).to have_http_status 200 + end + end + + describe 'GET #action' do + context 'valid' do + it 'renders action page with valid data' do + get action_activejob_web_job_job_execution_job_approval_request_path(job.id, job_execution.id, job_approval_request.id) + expect(response).to render_template('action') + expect(response).to have_http_status 200 + end + end + + context 'invalid' do + it 'should not render action page with invalid data' do + expect do + get action_activejob_web_job_job_execution_job_approval_request_path(SecureRandom.uuid, 1, 1) + end.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + describe 'PATCH #update' do + context 'valid' do + it 'valid update of job approval request' do + patch activejob_web_job_job_execution_job_approval_request_path(job, job_execution, job_approval_request), + params: { activejob_web_job_approval_request: { response: 0, approver_comments: 'Test comments' } } + job_approval_request.reload + expect(job_approval_request.response).to eq(0) + expect(job_approval_request.approver_comments).to eq('Test comments') + expect(response).to have_http_status 302 + expect(flash[:notice]).to eq('Job approval request updated successfully.') + expect(response).to redirect_to(activejob_web_job_job_execution_job_approval_requests_path(job, job_execution)) + end + end + end +end diff --git a/spec/requests/activejob_web/job_executions_spec.rb b/spec/requests/activejob_web/job_executions_spec.rb new file mode 100644 index 0000000..4ca6ed4 --- /dev/null +++ b/spec/requests/activejob_web/job_executions_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'rails_helper' +RSpec.describe ActivejobWeb::JobExecutionsController, type: :request do + let(:job) { create(:job) } # Assuming you have a Job model + let!(:user) { create(:user) } + let(:execution) { create(:job_execution, job:, requestor_id: user.id) } + before do + allow_any_instance_of(ActivejobWeb::JobsHelper).to receive(:activejob_web_current_user).and_return(user) + end + describe 'GET #index' do + it 'renders the index template' do + get activejob_web_job_job_executions_path(job_id: job.id) + + expect(response).to have_http_status 200 + expect(response).to render_template(:index) + end + end + + describe 'GET #show' do + it 'renders the show template' do + get activejob_web_job_job_execution_path(job_id: job.id, id: execution.id) + expect(response).to have_http_status 200 + expect(response).to render_template(:show) + end + end + + describe 'GET #edit' do + it 'renders the edit template' do + get edit_activejob_web_job_job_execution_path(job, execution) + + expect(response).to have_http_status 200 + expect(response).to render_template(:edit) + end + end + + describe 'POST #create' do + context 'with valid parameters' do + let(:valid_execution_attributes) { attributes_for(:valid_job_execution) } + + it 'creates a new job execution' do + post activejob_web_job_job_executions_path(job), params: { + activejob_web_job_execution: valid_execution_attributes + } + + expect(response).to have_http_status(302) # Redirect status + expect(flash[:notice]).to eq('Job execution created successfully.') + expect(response).to redirect_to(activejob_web_job_job_executions_path(job)) + end + end + + context 'with invalid parameters' do + let(:invalid_execution_attributes) { attributes_for(:invalid_job_execution) } + + it 'renders the index template' do + post activejob_web_job_job_executions_path(job), params: { + activejob_web_job_execution: invalid_execution_attributes + } + + expect(response).to have_http_status(200) # Success status since it renders the index template + expect(response).to render_template(:index) + end + end + end + describe 'PATCH #update' do + context 'with valid parameters' do + it 'updates the job execution and redirects to the show page' do + patch activejob_web_job_job_execution_path(job, execution), + params: { activejob_web_job_execution: { status: 'requested' } } + + expect(response).to have_http_status 302 # Redirect status + expect(flash[:notice]).to eq('Job execution was successfully updated.') + expect(response).to redirect_to(activejob_web_job_job_execution_path(job)) + end + end + end +end diff --git a/spec/requests/activejob_web/jobs_spec.rb b/spec/requests/activejob_web/jobs_spec.rb new file mode 100644 index 0000000..d393a1f --- /dev/null +++ b/spec/requests/activejob_web/jobs_spec.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivejobWeb::JobsController, type: :request do + let(:valid_attributes) { { title: 'Activejob', description: 'Web Gem' } } + let(:job) { create(:job) } + let!(:user) { create(:user) } + before do + allow_any_instance_of(ActivejobWeb::JobsHelper).to receive(:activejob_web_current_user).and_return(user) + end + describe 'GET /index' do + context 'returns a successful response' do + it 'Valid index' do + get activejob_web_jobs_path + expect(response).to render_template('index') + expect(response).to have_http_status 200 + end + end + end + + describe 'GET #show' do + context 'returns a successful response' do + it 'Valid show' do + job.executors << user + get activejob_web_job_path(job.id) + expect(response).to render_template('show') + expect(response).to have_http_status 200 + end + end + end + + describe 'GET #edit' do + context 'valid' do + it 'valid edit' do + get edit_activejob_web_job_path(job.id) + expect(response).to have_http_status 200 + end + end + + context 'invalid' do + it 'invalid edit without parameters' do + expect { get edit_activejob_web_job_path(SecureRandom.uuid) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + describe 'PATCH #update' do + let(:approver) { create(:approver) } + let(:executor) { create(:executor) } + let(:approver1) { create(:approver1) } + let(:executor1) { create(:executor1) } + context 'valid' do + it 'valid update of job with approvers and executors gets assigned' do + patch activejob_web_job_path(job.id), + params: { id: job.id, activejob_web_job: valid_attributes.merge(approver_ids: [approver.id], executor_ids: [executor.id]) } + job.reload + expect(response).to redirect_to(activejob_web_job_path(job)) + expect(job.approvers).to include(approver) + expect(job.executors).to include(executor) + end + + it 'valid update of job with removal of assigned approvers and executors' do + job1 = ActivejobWeb::Job.create(valid_attributes.merge(approver_ids: [approver1.id, approver.id], executor_ids: [executor1.id, executor.id])) + expect(job1.approvers).to include(approver1) + expect(job1.executors).to include(executor1) + patch activejob_web_job_path(job1), + params: { id: job1.id, activejob_web_job: valid_attributes.merge(approver_ids: [approver.id], executor_ids: [executor.id]) } + job1.reload + expect(response).to redirect_to(activejob_web_job_path(job1)) + expect(job1.approvers).not_to include(approver1) + expect(job1.executors).not_to include(executor1) + end + end + + context 'invalid' do + it 'should not update job with invalid approvers and executors' do + expect do + patch activejob_web_job_path(job.id), + params: { id: job.id, activejob_web_job: valid_attributes.merge(approver_ids: ['4'], executor_ids: ['Test']) } + end.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..409c64b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end diff --git a/test/activejob/web_test.rb b/test/activejob/web_test.rb index 8593d80..98dcd1e 100644 --- a/test/activejob/web_test.rb +++ b/test/activejob/web_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'test_helper' module Activejob diff --git a/test/controllers/activejob_web/jobs_controller_test.rb b/test/controllers/activejob_web/jobs_controller_test.rb new file mode 100644 index 0000000..f806d64 --- /dev/null +++ b/test/controllers/activejob_web/jobs_controller_test.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'test_helper' + +module ActivejobWeb + class JobsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end + end +end diff --git a/test/controllers/activejob_web_jobs_controller_test.rb b/test/controllers/activejob_web_jobs_controller_test.rb new file mode 100644 index 0000000..8a57acb --- /dev/null +++ b/test/controllers/activejob_web_jobs_controller_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class ActivejobWebJobsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/dummy/Rakefile b/test/dummy/Rakefile index e85f913..488c551 100644 --- a/test/dummy/Rakefile +++ b/test/dummy/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. diff --git a/test/dummy/app/channels/application_cable/channel.rb b/test/dummy/app/channels/application_cable/channel.rb index d672697..9aec230 100644 --- a/test/dummy/app/channels/application_cable/channel.rb +++ b/test/dummy/app/channels/application_cable/channel.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/test/dummy/app/channels/application_cable/connection.rb b/test/dummy/app/channels/application_cable/connection.rb index 0ff5442..8d6c2a1 100644 --- a/test/dummy/app/channels/application_cable/connection.rb +++ b/test/dummy/app/channels/application_cable/connection.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/test/dummy/app/controllers/application_controller.rb b/test/dummy/app/controllers/application_controller.rb index 09705d1..7944f9f 100644 --- a/test/dummy/app/controllers/application_controller.rb +++ b/test/dummy/app/controllers/application_controller.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base end diff --git a/test/dummy/app/helpers/application_helper.rb b/test/dummy/app/helpers/application_helper.rb index de6be79..15b06f0 100644 --- a/test/dummy/app/helpers/application_helper.rb +++ b/test/dummy/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module ApplicationHelper end diff --git a/test/dummy/app/helpers/authentication_helper.rb b/test/dummy/app/helpers/authentication_helper.rb new file mode 100644 index 0000000..97cbcf1 --- /dev/null +++ b/test/dummy/app/helpers/authentication_helper.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module AuthenticationHelper + def my_app_current_user + # Specify the current user here + current_user + end +end diff --git a/test/dummy/app/jobs/application_job.rb b/test/dummy/app/jobs/application_job.rb index d394c3d..bef3959 100644 --- a/test/dummy/app/jobs/application_job.rb +++ b/test/dummy/app/jobs/application_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationJob < ActiveJob::Base # Automatically retry jobs that encountered a deadlock # retry_on ActiveRecord::Deadlocked diff --git a/test/dummy/app/mailers/application_mailer.rb b/test/dummy/app/mailers/application_mailer.rb index 286b223..d84cb6e 100644 --- a/test/dummy/app/mailers/application_mailer.rb +++ b/test/dummy/app/mailers/application_mailer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationMailer < ActionMailer::Base default from: 'from@example.com' layout 'mailer' diff --git a/test/dummy/app/models/application_record.rb b/test/dummy/app/models/application_record.rb index b63caeb..08dc537 100644 --- a/test/dummy/app/models/application_record.rb +++ b/test/dummy/app/models/application_record.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationRecord < ActiveRecord::Base primary_abstract_class end diff --git a/test/dummy/app/models/author.rb b/test/dummy/app/models/author.rb new file mode 100644 index 0000000..55c5814 --- /dev/null +++ b/test/dummy/app/models/author.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Author < ApplicationRecord + validates :name, presence: true, length: { maximum: 5 } +end diff --git a/test/dummy/app/models/user.rb b/test/dummy/app/models/user.rb new file mode 100644 index 0000000..2f0d250 --- /dev/null +++ b/test/dummy/app/models/user.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +class User < ApplicationRecord + extend ActivejobWeb::JobsHelper + # == Associations ================================================================================================== + has_and_belongs_to_many :executors, class_name: 'ActivejobWeb::Job', join_table: 'activejob_web_job_executors', + association_foreign_key: 'executor_id' + has_many :job_approval_requests + + # == Validations =================================================================================================== + validates :name, presence: true, length: { maximum: 50 } + + # == Scopes ======================================================================================================== + scope :super_admin_users, -> { User.all.limit(1) } + + def self.allow_admin_access? + lambda do |_request| + super_admin_users.include?(activejob_web_current_user) + end + end +end diff --git a/test/dummy/app/views/layouts/application.html.erb b/test/dummy/app/views/layouts/application.html.erb index f72b4ef..51e8dd7 100644 --- a/test/dummy/app/views/layouts/application.html.erb +++ b/test/dummy/app/views/layouts/application.html.erb @@ -1,7 +1,10 @@ - + -