diff --git a/.gitignore b/.gitignore index e16dc71..8a0a6a0 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ # Ignore master key for decrypting credentials and more. /config/master.key + +# Ignore Rubymine files +.idea/ 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 new file mode 100644 index 0000000..3cc00bb --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,34 @@ +require: + - rubocop-rails + +AllCops: + TargetRubyVersion: 3.1.2 + Exclude: + - 'app/admin/**/*' + - 'bin/**/*' + - 'vendor/**/*' + - 'db/**/*' + - 'lib/tasks/**/*' + - 'config/**/*' + - 'spec/rails_helper.rb' + - 'spec/spec_helper.rb' +Documentation: + Enabled: false + +Style/FrozenStringLiteralComment: + Enabled: false + +Lint/MissingSuper: + Enabled: false + +Metrics/BlockLength: + ExcludedMethods: ['describe', 'context', 'feature', 'scenario', 'let', 'register', 'path'] + +Metrics/LineLength: + Max: 120 + +Metrics/MethodLength: + Max: 20 + +AsciiComments: + Enabled: false \ No newline at end of file diff --git a/Gemfile b/Gemfile index e2207f7..75e659f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,72 +1,43 @@ -source "https://rubygems.org" +source 'https://rubygems.org' git_source(:github) { |repo| "https://github.com/#{repo}.git" } -ruby "3.1.2" - -# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main" -gem "rails", "~> 7.0.8" - -# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails] -gem "sprockets-rails" - -# Use postgresql as the database for Active Record -gem "pg", "~> 1.1" - -# Use the Puma web server [https://github.com/puma/puma] -gem "puma", "~> 5.0" - -# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails] -gem "importmap-rails" - -# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev] -gem "turbo-rails" - -# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev] -gem "stimulus-rails" - -# Build JSON APIs with ease [https://github.com/rails/jbuilder] -gem "jbuilder" - -# Use Redis adapter to run Action Cable in production -gem "redis", "~> 4.0" - -# Use Kredis to get higher-level data types in Redis [https://github.com/rails/kredis] -# gem "kredis" - -# Use Active Model has_secure_password [https://guides.rubyonrails.org/active_model_basics.html#securepassword] -# gem "bcrypt", "~> 3.1.7" - -# Windows does not include zoneinfo files, so bundle the tzinfo-data gem -gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ] - -# Reduces boot times through caching; required in config/boot.rb -gem "bootsnap", require: false - -# Use Sass to process CSS -# gem "sassc-rails" - -# Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -# gem "image_processing", "~> 1.2" +ruby '3.1.2' + +gem 'bootsnap', require: false +gem 'foreman', '~> 0.87.2' +gem 'importmap-rails' +gem 'jbuilder' +gem 'pg', '~> 1.1' +gem 'puma', '~> 5.0' +gem 'rails', '~> 7.0.8' +gem 'redis', '~> 4.0' +gem 'sprockets-rails' +gem 'stimulus-rails' +gem 'turbo-rails' +gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] group :development, :test do - # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem - gem "debug", platforms: %i[ mri mingw x64_mingw ] + gem 'database_cleaner', '~> 2.0', '>= 2.0.2' + gem 'debug', platforms: %i[mri mingw x64_mingw] + gem 'factory_bot', '~> 6.4', '>= 6.4.5' + gem 'factory_bot_rails', '~> 6.2' + gem 'rubocop', require: false + gem 'rubocop-capybara', '~> 2.19' + gem 'rubocop-rails', require: false + gem 'rubocop-rspec', '~> 2.25' end group :development do - # Use console on exceptions pages [https://github.com/rails/web-console] - gem "web-console" - - # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler] - # gem "rack-mini-profiler" - - # Speed up commands on slow machines / big apps [https://github.com/rails/spring] - # gem "spring" + gem 'web-console' end group :test do - # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing] - gem "capybara" - gem "selenium-webdriver" - + gem 'capybara' + gem 'ffaker', '~> 2.21' + gem 'rails-controller-testing' + gem 'rspec-rails' + gem 'selenium-webdriver' + gem 'shoulda-matchers', '~> 6.0' end + +gem 'tailwindcss-rails', '~> 2.3' diff --git a/Gemfile.lock b/Gemfile.lock index 823c3b2..74573c0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -68,6 +68,7 @@ GEM tzinfo (~> 2.0) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) bindex (0.8.1) bootsnap (1.17.1) msgpack (~> 1.2) @@ -83,11 +84,25 @@ GEM xpath (~> 3.2) concurrent-ruby (1.2.3) crass (1.0.6) + database_cleaner (2.0.2) + database_cleaner-active_record (>= 2, < 3) + database_cleaner-active_record (2.1.0) + activerecord (>= 5.a) + database_cleaner-core (~> 2.0.0) + database_cleaner-core (2.0.1) date (3.3.4) debug (1.9.1) irb (~> 1.10) reline (>= 0.3.8) + diff-lcs (1.5.0) erubi (1.12.0) + factory_bot (6.4.5) + activesupport (>= 5.0.0) + factory_bot_rails (6.4.3) + factory_bot (~> 6.4) + railties (>= 5.0.0) + ffaker (2.23.0) + foreman (0.87.2) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.1) @@ -103,6 +118,8 @@ GEM jbuilder (2.11.5) actionview (>= 5.0.0) activesupport (>= 5.0.0) + json (2.7.1) + language_server-protocol (3.17.0.3) loofah (2.22.0) crass (~> 1.0.2) nokogiri (>= 1.12.0) @@ -129,6 +146,10 @@ GEM nio4r (2.7.0) nokogiri (1.16.0-x86_64-linux) racc (~> 1.4) + parallel (1.24.0) + parser (3.3.0.3) + ast (~> 2.4.1) + racc pg (1.5.4) psych (5.1.2) stringio @@ -153,6 +174,10 @@ GEM activesupport (= 7.0.8) bundler (>= 1.15.0) railties (= 7.0.8) + 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 @@ -167,6 +192,7 @@ GEM rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) + rainbow (3.1.1) rake (13.1.0) rdoc (6.6.2) psych (>= 4.0.0) @@ -175,11 +201,57 @@ GEM reline (0.4.2) io-console (~> 0.5) rexml (3.2.6) + 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.1.0) + 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.59.0) + json (~> 2.3) + language_server-protocol (>= 3.17.0) + parallel (~> 1.10) + parser (>= 3.2.2.4) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.30.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.30.0) + parser (>= 3.2.1.0) + rubocop-capybara (2.20.0) + rubocop (~> 1.41) + rubocop-factory_bot (2.25.1) + rubocop (~> 1.41) + rubocop-rails (2.23.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.30.0, < 2.0) + rubocop-rspec (2.26.1) + rubocop (~> 1.40) + rubocop-capybara (~> 2.17) + rubocop-factory_bot (~> 2.22) + ruby-progressbar (1.13.0) rubyzip (2.3.2) selenium-webdriver (4.16.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) + shoulda-matchers (6.0.0) + activesupport (>= 5.2.0) sprockets (4.2.1) concurrent-ruby (~> 1.0) rack (>= 2.2.4, < 4) @@ -190,6 +262,8 @@ GEM stimulus-rails (1.3.3) railties (>= 6.0.0) stringio (3.1.0) + tailwindcss-rails (2.3.0-x86_64-linux) + railties (>= 6.0.0) thor (1.3.0) timeout (0.4.1) turbo-rails (1.5.0) @@ -198,6 +272,7 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + unicode-display_width (2.5.0) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -217,16 +292,29 @@ PLATFORMS DEPENDENCIES bootsnap capybara + database_cleaner (~> 2.0, >= 2.0.2) debug + factory_bot (~> 6.4, >= 6.4.5) + factory_bot_rails (~> 6.2) + ffaker (~> 2.21) + foreman (~> 0.87.2) importmap-rails jbuilder pg (~> 1.1) puma (~> 5.0) rails (~> 7.0.8) + rails-controller-testing redis (~> 4.0) + rspec-rails + rubocop + rubocop-capybara (~> 2.19) + rubocop-rails + rubocop-rspec (~> 2.25) selenium-webdriver + shoulda-matchers (~> 6.0) sprockets-rails stimulus-rails + tailwindcss-rails (~> 2.3) turbo-rails tzinfo-data web-console diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 0000000..023e98a --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,2 @@ +web: bin/rails server -p 3000 +css: bin/rails tailwindcss:watch diff --git a/Rakefile b/Rakefile index 9a5ea73..e85f913 100644 --- a/Rakefile +++ b/Rakefile @@ -1,6 +1,6 @@ # 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. -require_relative "config/application" +require_relative 'config/application' Rails.application.load_tasks diff --git a/app/assets/builds/tailwind.css b/app/assets/builds/tailwind.css new file mode 100644 index 0000000..199c36f --- /dev/null +++ b/app/assets/builds/tailwind.css @@ -0,0 +1 @@ +/*! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:Inter var,ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid #0000;outline-offset:2px}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:#0000}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=checkbox]:indeterminate,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:#0000}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:#0000}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.mx-auto{margin-left:auto;margin-right:auto}.mt-28{margin-top:7rem}.block{display:block}.inline{display:inline}.flex{display:flex}.table{display:table}.min-w-full{min-width:100%}.flex-col{flex-direction:column}.overflow-x-auto{overflow-x:auto}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.hover\:underline:hover{text-decoration-line:underline} \ No newline at end of file diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index ddd546a..b06fc42 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -2,3 +2,4 @@ //= link_directory ../stylesheets .css //= link_tree ../../javascript .js //= link_tree ../../../vendor/javascript .js +//= link_tree ../builds diff --git a/app/assets/images/.keep b/app/assets/images/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/assets/stylesheets/application.tailwind.css b/app/assets/stylesheets/application.tailwind.css new file mode 100644 index 0000000..8666d2f --- /dev/null +++ b/app/assets/stylesheets/application.tailwind.css @@ -0,0 +1,13 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* + +@layer components { + .btn-primary { + @apply py-2 px-4 bg-blue-200; + } +} + +*/ diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/controllers/municipes_controller.rb b/app/controllers/municipes_controller.rb new file mode 100644 index 0000000..d1a1d8f --- /dev/null +++ b/app/controllers/municipes_controller.rb @@ -0,0 +1,44 @@ +class MunicipesController < ApplicationController + before_action :set_municipe, only: %i[show edit update] + + def index + @municipes = Municipe.all + end + + def show; end + + def new + @municipe = Municipe.new + end + + def create + @municipe = Municipe.new(municipe_params) + + if @municipe.save + redirect_to @municipe, notice: t('.create') + else + render :new, status: :unprocessable_entity + end + end + + def edit; end + + def update + if @municipe.update(municipe_params) + redirect_to @municipe, notice: t('.update') + else + render :edit, status: :unprocessable_entity + end + end + + private + + def set_municipe + @municipe = Municipe.find(params[:id]) + end + + def municipe_params + params.require(:municipe).permit(:full_name, :cpf, :cns, :email, :email_confirmation, :birth_date, :phone_number, + :photo, :status) + end +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index 3c34c81..286b223 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,4 +1,4 @@ class ApplicationMailer < ActionMailer::Base - default from: "from@example.com" - layout "mailer" + default from: 'from@example.com' + layout 'mailer' end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/app/models/municipe.rb b/app/models/municipe.rb new file mode 100644 index 0000000..def7a8c --- /dev/null +++ b/app/models/municipe.rb @@ -0,0 +1,13 @@ +class Municipe < ApplicationRecord + enum status: { active: 0, inactive: 1 } + has_one_attached :photo + + validates :cpf, cpf: true + validates :cns, cns: true + validates :birth_date, birth_date: true + validates :email, confirmation: true, format: { with: URI::MailTo::EMAIL_REGEXP } + validates :email, confirmation: { case_sensitive: false } + + validates :full_name, :cpf, :cns, :email, :email_confirmation, :birth_date, :phone_number, :photo, :status, + presence: true +end diff --git a/app/validators/birth_date_validator.rb b/app/validators/birth_date_validator.rb new file mode 100644 index 0000000..8a8fca4 --- /dev/null +++ b/app/validators/birth_date_validator.rb @@ -0,0 +1,15 @@ +class BirthDateValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if birth_date_valid?(value) + + record.errors.add(attribute, options[:message] || :invalid_birth_date) + end + + private + + def birth_date_valid?(birth_date) + return false if birth_date.nil? || birth_date > Date.current || Date.current.year - birth_date.year > 116 + + true + end +end diff --git a/app/validators/cns_validator.rb b/app/validators/cns_validator.rb new file mode 100644 index 0000000..df672bd --- /dev/null +++ b/app/validators/cns_validator.rb @@ -0,0 +1,64 @@ +class CnsValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if cns_valid?(value) || prov_cns_valid?(value) + + record.errors.add(attribute, options[:message] || :invalid_cns) + end + + private + + def cns_valid?(cns) + return false unless cns && cns.strip.length == 15 + + pis = cns[0, 11] + sum = calculate_sum(pis) + + dv = calculate_dv(sum) + + result = if dv == 10 + calculate_result(pis, 2) + else + "#{pis}000#{dv.to_i}" + end + + cns == result + end + + def calculate_sum(pis) + sum = 0 + pis.each_char.with_index do |digit, index| + weight = 15 - index + sum += digit.to_i * weight + end + sum + end + + def calculate_dv(sum) + rest = sum % 11 + dv = 11 - rest + dv.zero? ? 0 : dv + end + + def calculate_result(pis, extra_sum) + sum = calculate_sum(pis) + sum += extra_sum + rest = sum % 11 + dv = 11 - rest + "#{pis}001#{dv.to_i}" + end + + def prov_cns_valid?(cns) + return false if cns.nil? || cns.strip.length != 15 + + sum = 0 + + cns.each_char.with_index do |digit, index| + weight = 15 - index + sum += digit.to_i * weight + end + + rest = sum % 11 + + rest.zero? + end +end diff --git a/app/validators/cpf_validator.rb b/app/validators/cpf_validator.rb new file mode 100644 index 0000000..d4b1e10 --- /dev/null +++ b/app/validators/cpf_validator.rb @@ -0,0 +1,28 @@ +class CpfValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return if cpf_valid?(value) + + record.errors.add(attribute, options[:message] || :invalid_cpf) + end + + private + + def cpf_valid?(cpf) + return false if cpf.nil? + + digits = cpf.scan(/[0-9]/).map(&:to_i) + + return false unless digits.length == 11 + + result1 = calculate_digit(digits, 9) + result2 = calculate_digit(digits, 10) + + result1 == digits[9] && result2 == digits[10] + end + + def calculate_digit(digits, position) + sum = digits.take(position).reverse_each.with_index(2).sum { |digit, i| digit * i } + sum -= (11 * (sum / 11)) + [0, 1].include?(sum) ? 0 : 11 - sum + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 3bab1c7..e50b407 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,12 +5,15 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> - <%= yield %> +
+ <%= yield %> +
diff --git a/app/views/municipes/_form.html.erb b/app/views/municipes/_form.html.erb new file mode 100644 index 0000000..1c41b7f --- /dev/null +++ b/app/views/municipes/_form.html.erb @@ -0,0 +1,61 @@ +<%= form_with(model: municipe, local: true, html: { multipart: true }) do |form| %> + + <% if municipe.errors.any? %> +
+

<%= "#{pluralize(municipe.errors.count, t('activerecord.errors.header.one'), t('activerecord.errors.header.other'))} #{pluralize(municipe.errors.count, t('activerecord.errors.prohibited.one'), t('activerecord.errors.prohibited.other'))} #{t('activerecord.errors.body')}" %>

+ +
+ <% end %> + +
+ <%= form.label :photo, style: "display: block" %> + <%= form.file_field :photo %> +
+
+ <%= form.label :full_name, style: "display: block" %> + <%= form.text_field :full_name %> +
+ +
+ <%= form.label :cpf, style: "display: block" %> + <%= form.text_field :cpf, placeholder: '000.000.000-00', maxlength: 14 %> +
+ +
+ <%= form.label :cns, style: "display: block" %> + <%= form.text_field :cns %> +
+ +
+ <%= form.label :email, style: "display: block" %> + <%= form.text_field :email %> +
+ +
+ <%= form.label :email_confirmation, style: "display: block" %> + <%= form.text_field :email_confirmation %> +
+ +
+ <%= form.label :birth_date, style: "display: block" %> + <%= form.date_field :birth_date %> +
+ +
+ <%= form.label :phone_number, style: "display: block" %> + <%= form.text_field :phone_number %> +
+ +
+ <%= form.label :status, 'Status', style: "display: block" %> + <%= form.select :status, Municipe.statuses.keys.map { |s| [I18n.t("activerecord.attributes.municipe.status.#{s}"), s] } %> +
+ +
+ <%= form.submit t('.save') %> +
+<% end %> diff --git a/app/views/municipes/_municipe.html.erb b/app/views/municipes/_municipe.html.erb new file mode 100644 index 0000000..6cc71a3 --- /dev/null +++ b/app/views/municipes/_municipe.html.erb @@ -0,0 +1,14 @@ + + <%= image_tag municipe.photo if municipe.photo.attached? %> + <%= municipe.full_name %> + <%= municipe.cpf %> + <%= municipe.cns %> + <%= municipe.email %> + <%= I18n.l(municipe.birth_date) %> + <%= municipe.phone_number %> + <%= t("activerecord.attributes.municipe.status.#{municipe.status}") %> + + <%= link_to t('.show'), municipe, class: 'hover:underline' %> + <%= link_to t('.edit'), edit_municipe_path(municipe), class: 'hover:underline' %> + + diff --git a/app/views/municipes/edit.html.erb b/app/views/municipes/edit.html.erb new file mode 100644 index 0000000..ca3efa4 --- /dev/null +++ b/app/views/municipes/edit.html.erb @@ -0,0 +1,12 @@ +
+

<%= t('.title') %>

+ + <%= render "form", municipe: @municipe %> + +
+ +
+ <%= link_to t('.show'), @municipe %> | + <%= link_to t('.back'), municipes_path %> +
+
\ No newline at end of file diff --git a/app/views/municipes/index.html.erb b/app/views/municipes/index.html.erb new file mode 100644 index 0000000..64d20a8 --- /dev/null +++ b/app/views/municipes/index.html.erb @@ -0,0 +1,28 @@ +
+

<%= notice %>

+ +

<%= t('.title') %>

+ +
+ + + + + + + + + + + + + + + + <%= render @municipes %> + +
<%= I18n.t('activerecord.attributes.municipe.photo') %><%= I18n.t('activerecord.attributes.municipe.full_name') %><%= I18n.t('activerecord.attributes.municipe.cpf') %><%= I18n.t('activerecord.attributes.municipe.cns') %><%= I18n.t('activerecord.attributes.municipe.email') %><%= I18n.t('activerecord.attributes.municipe.birth_date') %><%= I18n.t('activerecord.attributes.municipe.phone_number') %><%= t('.status') %><%= t('.actions') %>
+
+ + <%= link_to t('.add_municipe'), new_municipe_path %> +
\ No newline at end of file diff --git a/app/views/municipes/new.html.erb b/app/views/municipes/new.html.erb new file mode 100644 index 0000000..06b585b --- /dev/null +++ b/app/views/municipes/new.html.erb @@ -0,0 +1,11 @@ +
+

<%= t('.title') %>

+ + <%= render "form", municipe: @municipe %> + +
+ +
+ <%= link_to t('.back'), municipes_path %> +
+
diff --git a/app/views/municipes/show.html.erb b/app/views/municipes/show.html.erb new file mode 100644 index 0000000..2c88472 --- /dev/null +++ b/app/views/municipes/show.html.erb @@ -0,0 +1,6 @@ +

<%= notice %>

+
+

<%= t('.title') %>

+ <%= render @municipe %> + <%= link_to t('.back'), municipes_path %> +
diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..ad72c7d --- /dev/null +++ b/bin/dev @@ -0,0 +1,16 @@ +#!/usr/bin/env sh + +if ! gem list foreman -i --silent; then + echo "Installing foreman..." + gem install foreman +fi + +# Default to port 3000 if not specified +export PORT="${PORT:-3000}" + +# Let the debug gem allow remote connections, +# but avoid loading until `debugger` is called +export RUBY_DEBUG_OPEN="true" +export RUBY_DEBUG_LAZY="true" + +exec foreman start -f Procfile.dev "$@" diff --git a/config.ru b/config.ru index 4a3c09a..ad1fbf2 100644 --- a/config.ru +++ b/config.ru @@ -1,6 +1,6 @@ # This file is used by Rack-based servers to start the application. -require_relative "config/environment" +require_relative 'config/environment' run Rails.application Rails.application.load_server diff --git a/config/application.rb b/config/application.rb index 16e4094..959df3e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -11,6 +11,9 @@ class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 7.0 + # Load validators for form fields + config.autoload_paths += %W(#{config.root}/app/validators) + # Configuration for the application, engines, and railties goes here. # # These settings can be overridden in specific environments using the files diff --git a/config/database.yml b/config/database.yml index 14490d5..bd008dc 100644 --- a/config/database.yml +++ b/config/database.yml @@ -17,8 +17,8 @@ default: &default adapter: postgresql encoding: unicode - # For details on connection pooling, see Rails configuration guide - # https://guides.rubyonrails.org/configuring.html#database-pooling + username: postgres + password: postgres pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %> development: diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb new file mode 100644 index 0000000..ba55edd --- /dev/null +++ b/config/initializers/locale.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Where the I18n library should search for translation files +I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] + +# Permitted locales available for the application +I18n.available_locales = [:en, 'pt-BR'] + +# Set default locale to something other than :en +I18n.default_locale = 'pt-BR' diff --git a/config/locales/controllers/municipes.pt-BR.yml b/config/locales/controllers/municipes.pt-BR.yml new file mode 100644 index 0000000..9f4c12f --- /dev/null +++ b/config/locales/controllers/municipes.pt-BR.yml @@ -0,0 +1,4 @@ +pt-BR: + municipes: + create: 'Cidadão cadastrado com sucesso.' + update: 'Cidadão atualizado com sucesso.' diff --git a/config/locales/defaults/pt-BR.yml b/config/locales/defaults/pt-BR.yml new file mode 100644 index 0000000..d16835f --- /dev/null +++ b/config/locales/defaults/pt-BR.yml @@ -0,0 +1,224 @@ +pt-BR: + activerecord: + errors: + messages: + record_invalid: 'A validação falhou: %{errors}' + restrict_dependent_destroy: + has_one: Não é possível excluir o registro pois existe um %{record} dependente + has_many: Não é possível excluir o registro pois existem %{record} dependentes + date: + abbr_day_names: + - dom + - seg + - ter + - qua + - qui + - sex + - sáb + abbr_month_names: + - + - jan + - fev + - mar + - abr + - mai + - jun + - jul + - ago + - set + - out + - nov + - dez + day_names: + - domingo + - segunda-feira + - terça-feira + - quarta-feira + - quinta-feira + - sexta-feira + - sábado + formats: + default: "%d/%m/%Y" + long: "%d de %B de %Y" + short: "%d de %B" + month_names: + - + - janeiro + - fevereiro + - março + - abril + - maio + - junho + - julho + - agosto + - setembro + - outubro + - novembro + - dezembro + order: + - :day + - :month + - :year + datetime: + distance_in_words: + about_x_hours: + one: aproximadamente %{count} hora + other: aproximadamente %{count} horas + about_x_months: + one: aproximadamente %{count} mês + other: aproximadamente %{count} meses + about_x_years: + one: aproximadamente %{count} ano + other: aproximadamente %{count} anos + almost_x_years: + one: quase %{count} ano + other: quase %{count} anos + half_a_minute: meio minuto + less_than_x_seconds: + one: menos de %{count} segundo + other: menos de %{count} segundos + less_than_x_minutes: + one: menos de um minuto + other: menos de %{count} minutos + over_x_years: + one: mais de %{count} ano + other: mais de %{count} anos + x_seconds: + one: "%{count} segundo" + other: "%{count} segundos" + x_minutes: + one: "%{count} minuto" + other: "%{count} minutos" + x_days: + one: "%{count} dia" + other: "%{count} dias" + x_months: + one: "%{count} mês" + other: "%{count} meses" + x_years: + one: "%{count} ano" + other: "%{count} anos" + prompts: + second: Segundo + minute: Minuto + hour: Hora + day: Dia + month: Mês + year: Ano + errors: + format: "%{attribute} %{message}" + messages: + accepted: deve ser aceito + blank: não pode ficar em branco + confirmation: não é igual a %{attribute} + empty: não pode ficar vazio + equal_to: deve ser igual a %{count} + even: deve ser par + exclusion: não está disponível + greater_than: deve ser maior que %{count} + greater_than_or_equal_to: deve ser maior ou igual a %{count} + in: deve estar em %{count} + inclusion: não está incluído na lista + invalid: não é válido + less_than: deve ser menor que %{count} + less_than_or_equal_to: deve ser menor ou igual a %{count} + model_invalid: 'A validação falhou: %{errors}' + not_a_number: não é um número + not_an_integer: não é um número inteiro + odd: deve ser ímpar + other_than: deve ser diferente de %{count} + present: deve ficar em branco + required: é obrigatório(a) + taken: já está em uso + too_long: + one: 'é muito longo (máximo: %{count} caracter)' + other: 'é muito longo (máximo: %{count} caracteres)' + too_short: + one: 'é muito curto (mínimo: %{count} caracter)' + other: 'é muito curto (mínimo: %{count} caracteres)' + wrong_length: + one: não possui o tamanho esperado (%{count} caracter) + other: não possui o tamanho esperado (%{count} caracteres) + template: + body: 'Por favor, verifique o(s) seguinte(s) campo(s):' + header: + one: 'Não foi possível gravar %{model}: %{count} erro' + other: 'Não foi possível gravar %{model}: %{count} erros' + helpers: + select: + prompt: Por favor selecione + submit: + create: Criar %{model} + submit: Salvar %{model} + update: Atualizar %{model} + number: + currency: + format: + delimiter: "." + format: "%u %n" + precision: 2 + separator: "," + significant: false + strip_insignificant_zeros: false + unit: R$ + format: + delimiter: "." + precision: 3 + round_mode: default + separator: "," + significant: false + strip_insignificant_zeros: false + human: + decimal_units: + format: "%n %u" + units: + billion: + one: bilhão + other: bilhões + million: + one: milhão + other: milhões + quadrillion: + one: quatrilhão + other: quatrilhões + thousand: mil + trillion: + one: trilhão + other: trilhões + unit: '' + format: + delimiter: '' + precision: 3 + significant: true + strip_insignificant_zeros: true + storage_units: + format: "%n %u" + units: + byte: + one: Byte + other: Bytes + eb: EB + gb: GB + kb: KB + mb: MB + pb: PB + tb: TB + percentage: + format: + delimiter: "." + format: "%n%" + precision: + format: + delimiter: "." + support: + array: + last_word_connector: " e " + two_words_connector: " e " + words_connector: ", " + time: + am: am + formats: + default: "%a, %d de %B de %Y, %H:%M:%S %z" + long: "%d de %B de %Y, %H:%M" + short: "%d de %B, %H:%M" + pm: pm \ No newline at end of file diff --git a/config/locales/models/active_model.pt-BR.yml b/config/locales/models/active_model.pt-BR.yml new file mode 100644 index 0000000..42eba0a --- /dev/null +++ b/config/locales/models/active_model.pt-BR.yml @@ -0,0 +1,9 @@ +pt-BR: + activemodel: + errors: + models: + validatable: + invalid_cpf: 'não é um CPF válido' + invalid_cns: 'não é um CNS válido' + invalid_email: 'não é um endereço de e-mail válido' + invalid_birth_date: 'não é uma data de nascimento válida' \ No newline at end of file diff --git a/config/locales/models/municipes.pt-BR.yml b/config/locales/models/municipes.pt-BR.yml new file mode 100644 index 0000000..e15be02 --- /dev/null +++ b/config/locales/models/municipes.pt-BR.yml @@ -0,0 +1,35 @@ +pt-BR: + activerecord: + models: + municipe: + one: 'Cidadão' + other: 'Cidadãos' + errors: + models: + municipe: + invalid_cpf: 'não é um cpf válido' + invalid_cns: 'não é um cns válido' + invalid_email: 'não é um email válido' + invalid_birth_date: 'não é uma data de nascimento válida' + header: + one: 'Erro' + other: 'Erros' + prohibited: + one: 'impediu' + other: 'impediram' + body: 'as informações desse cidadão serem salvas.' + attributes: + municipe: + photo: 'Foto' + full_name: 'Nome completo' + birth_date: 'Data de nascimento' + phone_number: 'Telefone' + cpf: 'CPF' + cns: 'CNS' + email: 'E-mail' + email_confirmation: 'Confirmação de E-mail' + status: + active: 'Ativo' + inactive: 'Inativo' + created_at: 'Criado em' + updated_at: 'Atualizado em' \ No newline at end of file diff --git a/config/locales/views/municipes.pt-BR.yml b/config/locales/views/municipes.pt-BR.yml new file mode 100644 index 0000000..fffce25 --- /dev/null +++ b/config/locales/views/municipes.pt-BR.yml @@ -0,0 +1,22 @@ +pt-BR: + municipes: + index: + title: 'Cidadãos' + actions: 'Ações' + add_municipe: 'Cadastrar cidadão' + status: 'Status' + edit: + title: 'Editando cidadão' + show: 'Mostrar' + back: 'Voltar' + municipe: + show: 'Mostrar' + edit: 'Editar' + new: + title: 'Cadastrando cidadão' + back: 'Voltar para cidadãos' + show: + title: 'Mostrando cidadão' + back: 'Voltar para cidadãos' + form: + save: 'Salvar' diff --git a/config/routes.rb b/config/routes.rb index 262ffd5..efe9e9f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,4 @@ Rails.application.routes.draw do - # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - - # Defines the root path route ("/") - # root "articles#index" + root to: 'municipes#index' + resources :municipes, except: [:destroy] end diff --git a/config/tailwind.config.js b/config/tailwind.config.js new file mode 100644 index 0000000..39364de --- /dev/null +++ b/config/tailwind.config.js @@ -0,0 +1,23 @@ +const defaultTheme = require('tailwindcss/defaultTheme') + +module.exports = { + content: [ + './public/*.html', + './app/helpers/**/*.rb', + './app/javascript/**/*.js', + './app/views/**/*.{erb,haml,html,slim}' + ], + theme: { + extend: { + fontFamily: { + sans: ['Inter var', ...defaultTheme.fontFamily.sans], + }, + }, + }, + plugins: [ + require('@tailwindcss/forms'), + require('@tailwindcss/aspect-ratio'), + require('@tailwindcss/typography'), + require('@tailwindcss/container-queries'), + ] +} diff --git a/db/migrate/20240118234524_create_municipes.rb b/db/migrate/20240118234524_create_municipes.rb new file mode 100644 index 0000000..dafbac1 --- /dev/null +++ b/db/migrate/20240118234524_create_municipes.rb @@ -0,0 +1,15 @@ +class CreateMunicipes < ActiveRecord::Migration[7.0] + def change + create_table :municipes do |t| + t.string :full_name + t.string :cpf + t.string :cns + t.string :email + t.date :birth_date + t.string :phone_number + t.integer :status + + t.timestamps + end + end +end diff --git a/db/migrate/20240119221515_create_active_storage_tables.active_storage.rb b/db/migrate/20240119221515_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..8a7bfe1 --- /dev/null +++ b/db/migrate/20240119221515_create_active_storage_tables.active_storage.rb @@ -0,0 +1,57 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + 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 + + 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 [ :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 + + 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 [ :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/seeds.rb b/db/seeds.rb index bc25fce..27a35cb 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,7 +1,23 @@ -# This file should contain all the record creation needed to seed the database with its default values. -# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup). -# -# Examples: -# -# movies = Movie.create([{ name: "Star Wars" }, { name: "Lord of the Rings" }]) -# Character.create(name: "Luke", movie: movies.first) +if Rails.env.development? + # Creates municipes + Municipe.create!(full_name: 'Marcelo Sangali da Silva', + cpf: '032.063.460-45', + cns: '274892941220002', + email: 'giovanni_rj@outlook.com', + email_confirmation: 'giovanni_rj@outlook.com', + birth_date: Date.strptime('23/11/1988', '%d/%m/%Y'), + phone_number: '5521929597661', + status: rand(0..1)) do |municipe| + municipe.photo.attach(io: File.open(Rails.root.join('spec/fixtures/images/profiles/perfil-masculino-1.jpg').to_s), filename: 'foto-homem-1.jpg') + end + Municipe.create!(full_name: 'Victoria Roscher', + cpf: '159.418.450-06', + cns: '165570383740009', + email: 'Victoria_ux@gmail.com', + email_confirmation: 'Victoria_ux@gmail.com', + birth_date: Date.strptime('09/01/1992', '%d/%m/%Y'), + phone_number: '5546989151067', + status: rand(0..1)) do |municipe| + municipe.photo.attach(io: File.open(Rails.root.join('spec/fixtures/images/profiles/perfil-feminino-1.jpg').to_s), filename: 'foto-mulher-1.jpg') + end +end diff --git a/lib/assets/.keep b/lib/assets/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/spec/controllers/municipes_controller_spec.rb b/spec/controllers/municipes_controller_spec.rb new file mode 100644 index 0000000..ae7700c --- /dev/null +++ b/spec/controllers/municipes_controller_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +RSpec.describe MunicipesController, type: :controller do + let!(:municipe) { create(:municipe) } + + describe 'GET #index' do + it 'renders the index template' do + get :index + expect(response).to render_template(:index) + end + + it 'assigns all municipes to @municipes' do + get :index + expect(assigns(:municipes)).to eq([municipe]) + end + end + + describe 'GET #new' do + it 'renders the new template' do + get :new + expect(response).to render_template(:new) + end + end + + describe 'POST #create' do + it 'redirects to the show page on success' do + post :create, params: { municipe: attributes_for(:municipe) } + expect(response).to redirect_to(assigns(:municipe)) + end + + it 'creates a new municipe' do + expect do + post :create, params: { municipe: attributes_for(:municipe) } + end.to change(Municipe, :count).by(1) + end + + it 'dont creates a new municipe with invalid params' do + expect do + post :create, params: { municipe: attributes_for(:municipe, cpf: '102.785.90056') } + end.to change(Municipe, :count).by(0) + end + end + + describe 'GET #edit' do + it 'renders the edit template' do + get :edit, params: { id: municipe.id } + expect(response).to render_template(:edit) + end + end + + describe 'PUT #update' do + it 'redirects to the show page on success' do + put :update, params: { id: municipe.id, municipe: attributes_for(:municipe) } + expect(response).to redirect_to(assigns(:municipe)) + end + + it 'updates the municipe with valid name' do + put :update, params: { id: municipe.id, municipe: attributes_for(:municipe, full_name: 'Philipe Rodrigues') } + municipe.reload + expect(response).to redirect_to(municipe) + expect(flash[:notice]).to eq(I18n.t('municipes.update')) + end + + it 'does not update the municipe with invalid params' do + put :update, params: { id: municipe.id, municipe: attributes_for(:municipe, email: 'Dev.@Philipe@outlook.com') } + municipe.reload + + expect(response).to render_template(:edit) + expect(response).to have_http_status(:unprocessable_entity) + end + end + + describe 'GET #show' do + it 'renders the show template' do + get :show, params: { id: municipe.id } + expect(response).to render_template(:show) + end + end +end diff --git a/spec/factories/municipes.rb b/spec/factories/municipes.rb new file mode 100644 index 0000000..408d850 --- /dev/null +++ b/spec/factories/municipes.rb @@ -0,0 +1,17 @@ +FactoryBot.define do + factory :municipe do + full_name { FFaker::NameBR.name } + cpf { FFaker::IdentificationBR.cpf } + cns { %w[228538870180008 121770956210018 865829129710004].sample } + email { FFaker::Internet.free_email } + email_confirmation { email } + birth_date { FFaker::Date.birthday } + phone_number { FFaker::PhoneNumberBR.international_mobile_phone_number } + + photo do + random_image_file = Dir.glob(Rails.root.join('spec/fixtures/images/profiles/*.jpg').to_s).sample + Rack::Test::UploadedFile.new(random_image_file, 'image/jpeg') + end + status { %i[active inactive].sample } + end +end diff --git a/spec/features/municipes_spec.rb b/spec/features/municipes_spec.rb new file mode 100644 index 0000000..47212fb --- /dev/null +++ b/spec/features/municipes_spec.rb @@ -0,0 +1,79 @@ +require 'rails_helper' + +RSpec.feature 'Municipes', js: true, type: :feature do + before do + create(:municipe, full_name: 'João Victor') + create(:municipe, full_name: 'Thais Veras') + end + + scenario 'list municipes' do + visit municipes_path + + expect(page).to have_text('João Victor') + expect(page).to have_text('Thais Veras') + end + + scenario 'create municipe' do + visit new_municipe_path + valid_cpf = '269.461.417-50' + valid_email = FFaker::Internet.free_email + + attach_file 'municipe_photo', 'spec/fixtures/images/profiles/perfil-masculino-2.jpg' + fill_in('municipe[full_name]', with: 'Rick Spohr') + fill_in('municipe[cpf]', with: valid_cpf) + fill_in('municipe[cns]', with: '750620311660008') + fill_in('municipe_email', with: valid_email) + fill_in('municipe_email_confirmation', with: valid_email) + fill_in('municipe_birth_date', with: '1996-09-27') + fill_in('municipe_phone_number', with: '5521966980366') + select(I18n.t('activerecord.attributes.municipe.status.active'), from: 'municipe_status') + click_button I18n.t('municipes.form.save') + + expect(page).to have_text('Rick Spohr') + expect(page).to have_text('269.461.417-50') + expect(page).to have_text('750620311660008') + expect(page).to have_text('27/09/1996') + expect(page).to have_text('5521966980366') + end + + scenario 'show municipe' do + visit municipes_path(Municipe.first) + + expect(page).to have_text('João Victor') + expect(page).to have_text('Thais Veras') + end + + scenario 'update municipe' do + valid_cpf = '822.420.720-05' + valid_email = FFaker::Internet.free_email + visit municipes_path + first('table tbody tr').click_link(I18n.t('municipes.municipe.edit')) + + fill_in 'municipe[full_name]', with: 'Lucas Borges' + fill_in('municipe[cpf]', with: valid_cpf) + fill_in('municipe[cns]', with: '838337659440018') + fill_in('municipe_email', with: valid_email) + fill_in('municipe_email_confirmation', with: valid_email) + click_button I18n.t('municipes.form.save') + + expect(page).to have_text('Lucas Borges') + expect(page).to have_text('822.420.720-05') + expect(page).to have_text('838337659440018') + expect(page).to have_text(valid_email) + end + + scenario 'not update municipe with invalid email' do + valid_cpf = '822.420.720-05' + visit municipes_path + first('table tbody tr').click_link(I18n.t('municipes.municipe.edit')) + + fill_in 'municipe[full_name]', with: 'Lucas Borges' + fill_in('municipe[cpf]', with: valid_cpf) + fill_in('municipe[cns]', with: '838337659440018') + fill_in('municipe_email', with: 'marcos_dev@gmail..com') + fill_in('municipe[email_confirmation]', with: 'marcos_dev@gmail..com') + click_button I18n.t('municipes.form.save') + + expect(page).to have_text(I18n.t('errors.messages.invalid')) + end +end diff --git a/spec/fixtures/images/profiles/perfil-feminino-1.jpg b/spec/fixtures/images/profiles/perfil-feminino-1.jpg new file mode 100644 index 0000000..4fab314 Binary files /dev/null and b/spec/fixtures/images/profiles/perfil-feminino-1.jpg differ diff --git a/spec/fixtures/images/profiles/perfil-feminino-2.jpg b/spec/fixtures/images/profiles/perfil-feminino-2.jpg new file mode 100644 index 0000000..3c7e74b Binary files /dev/null and b/spec/fixtures/images/profiles/perfil-feminino-2.jpg differ diff --git a/spec/fixtures/images/profiles/perfil-feminino-3.jpg b/spec/fixtures/images/profiles/perfil-feminino-3.jpg new file mode 100644 index 0000000..57668d2 Binary files /dev/null and b/spec/fixtures/images/profiles/perfil-feminino-3.jpg differ diff --git a/spec/fixtures/images/profiles/perfil-masculino-1.jpg b/spec/fixtures/images/profiles/perfil-masculino-1.jpg new file mode 100644 index 0000000..3170af6 Binary files /dev/null and b/spec/fixtures/images/profiles/perfil-masculino-1.jpg differ diff --git a/spec/fixtures/images/profiles/perfil-masculino-2.jpg b/spec/fixtures/images/profiles/perfil-masculino-2.jpg new file mode 100644 index 0000000..a7ab911 Binary files /dev/null and b/spec/fixtures/images/profiles/perfil-masculino-2.jpg differ diff --git a/spec/fixtures/images/profiles/perfil-masculino-3.jpg b/spec/fixtures/images/profiles/perfil-masculino-3.jpg new file mode 100644 index 0000000..205d62e Binary files /dev/null and b/spec/fixtures/images/profiles/perfil-masculino-3.jpg differ diff --git a/spec/models/municipe_spec.rb b/spec/models/municipe_spec.rb new file mode 100644 index 0000000..9b34799 --- /dev/null +++ b/spec/models/municipe_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe Municipe, type: :model do + it { should define_enum_for(:status).with_values(active: 0, inactive: 1) } + + describe 'validations' do + it { should validate_presence_of(:full_name) } + it { should validate_presence_of(:cpf) } + it { should validate_presence_of(:cns) } + it { should validate_presence_of(:email) } + it { should validate_presence_of(:email_confirmation) } + it { should validate_presence_of(:birth_date) } + it { should validate_presence_of(:phone_number) } + it { should validate_presence_of(:photo) } + end + + describe 'attachments' do + it { should have_one_attached(:photo) } + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..29763ff --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,75 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +require 'support/database_cleaner' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# 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. +# +# Rails.root.glob('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 + abort e.to_s.strip +end +RSpec.configure do |config| + # gem shoulda-matchers config + Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end + end + + # the line below is a config for factory bot + config.include FactoryBot::Syntax::Methods + + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = Rails.root.join('spec/fixtures') + + # 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! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..327b58e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,94 @@ +# 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. +=begin + # 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 +end diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb new file mode 100644 index 0000000..e0dbc9a --- /dev/null +++ b/spec/support/database_cleaner.rb @@ -0,0 +1,21 @@ +RSpec.configure do |config| + config.before(:suite) do + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each, js: true) do + DatabaseCleaner.strategy = :truncation + end + + config.before(:each) do + DatabaseCleaner.start + end + + config.after(:each) do + DatabaseCleaner.clean + end +end diff --git a/spec/validators/birth_date_validator_spec.rb b/spec/validators/birth_date_validator_spec.rb new file mode 100644 index 0000000..8ba247c --- /dev/null +++ b/spec/validators/birth_date_validator_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +class BirthDateValidatable + include ActiveModel::Model + attr_accessor :birth_date + + validates :birth_date, birth_date: true +end + +RSpec.describe BirthDateValidator do + subject(:validator) { described_class.new(attributes: [:birth_date]) } + + let(:model) { BirthDateValidatable.new } + + context 'when birth date is valid' do + valid_birth_dates = [Date.new(1990, 1, 1), Date.new(1985, 5, 10)] + + valid_birth_dates.each do |birth_date| + it "is valid for birth date: #{birth_date}" do + model.birth_date = birth_date + expect(model).to be_valid + end + end + end + + context 'when birth date is invalid' do + invalid_birth_dates = [Date.current + 1.day, Date.new(1900, 1, 1)] + + invalid_birth_dates.each do |birth_date| + it "is not valid for birth date: #{birth_date}" do + model.birth_date = birth_date + expect(model).not_to be_valid + end + end + end +end diff --git a/spec/validators/cns_validator_spec.rb b/spec/validators/cns_validator_spec.rb new file mode 100644 index 0000000..780b748 --- /dev/null +++ b/spec/validators/cns_validator_spec.rb @@ -0,0 +1,41 @@ +require 'rails_helper' + +class CnsValidatable + include ActiveModel::Model + attr_accessor :cns + + validates :cns, cns: true +end + +RSpec.describe CnsValidator do + subject(:validator) { described_class.new(attributes: [:cns]) } + let(:model) { CnsValidatable.new } + + context 'when CNS is valid' do + it 'is valid' do + model.cns = '170145603800018' + expect(model).to be_valid + end + end + + context 'when CNS is provisional valid' do + it 'is valid' do + model.cns = '170145603800018' + expect(model).to be_valid + end + end + + context 'when CNS is invalid' do + it 'is not valid' do + model.cns = '123456789012346' + expect(model).not_to be_valid + end + end + + context 'when CNS is provisional invalid' do + it 'is not valid' do + model.cns = '372837490874188' + expect(model).not_to be_valid + end + end +end diff --git a/spec/validators/cpf_validator_spec.rb b/spec/validators/cpf_validator_spec.rb new file mode 100644 index 0000000..700efab --- /dev/null +++ b/spec/validators/cpf_validator_spec.rb @@ -0,0 +1,36 @@ +require 'rails_helper' + +class CpfValidatable + include ActiveModel::Model + attr_accessor :cpf + + validates :cpf, cpf: true +end + +RSpec.describe CpfValidator do + subject(:validator) { described_class.new(attributes: [:cpf]) } + + let(:model) { CpfValidatable.new } + + context 'when CPF is valid' do + valid_cpfs = %w[441.778.037-40 501.693.187-96] + + valid_cpfs.each do |cpf| + it "is valid for CPF: #{cpf}" do + model.cpf = cpf + expect(model).to be_valid + end + end + end + + context 'when CPF is invalid' do + invalid_cpfs = %w[123.456.789-01 502.693.187-96] + + invalid_cpfs.each do |cpf| + it "is not valid for CPF: #{cpf}" do + model.cpf = cpf + expect(model).not_to be_valid + end + end + end +end diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb index d19212a..23701b4 100644 --- a/test/application_system_test_case.rb +++ b/test/application_system_test_case.rb @@ -1,4 +1,4 @@ -require "test_helper" +require 'test_helper' class ApplicationSystemTestCase < ActionDispatch::SystemTestCase driven_by :selenium, using: :chrome, screen_size: [1400, 1400] diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb index 800405f..f925925 100644 --- a/test/channels/application_cable/connection_test.rb +++ b/test/channels/application_cable/connection_test.rb @@ -1,11 +1,13 @@ -require "test_helper" +require 'test_helper' -class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase - # test "connects with cookies" do - # cookies.signed[:user_id] = 42 - # - # connect - # - # assert_equal connection.user_id, "42" - # end +module ApplicationCable + class ConnectionTest < ActionCable::Connection::TestCase + # test "connects with cookies" do + # cookies.signed[:user_id] = 42 + # + # connect + # + # assert_equal connection.user_id, "42" + # end + end end diff --git a/test/test_helper.rb b/test/test_helper.rb index d713e37..1b7300e 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,13 +1,15 @@ -ENV["RAILS_ENV"] ||= "test" -require_relative "../config/environment" -require "rails/test_help" +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +require 'rails/test_help' -class ActiveSupport::TestCase - # Run tests in parallel with specified workers - parallelize(workers: :number_of_processors) +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) - # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. - fixtures :all + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all - # Add more helper methods to be used by all tests here... + # Add more helper methods to be used by all tests here... + end end