diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
new file mode 100644
index 0000000..4671d69
--- /dev/null
+++ b/.github/workflows/linters.yml
@@ -0,0 +1,44 @@
+name: Linters
+
+on: pull_request
+
+env:
+ FORCE_COLOR: 1
+
+jobs:
+ rubocop:
+ name: Rubocop
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-ruby@v1
+ with:
+ ruby-version: 3.1.x
+ - name: Setup Rubocop
+ run: |
+ gem install --no-document rubocop -v '>= 1.0, < 2.0' # https://docs.rubocop.org/en/stable/installation/
+ [ -f .rubocop.yml ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/ror/.rubocop.yml
+ - name: Rubocop Report
+ run: rubocop --color
+ stylelint:
+ name: Stylelint
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: "18.x"
+ - name: Setup Stylelint
+ run: |
+ npm install --save-dev stylelint@13.x stylelint-scss@3.x stylelint-config-standard@21.x stylelint-csstree-validator@1.x
+ [ -f .stylelintrc.json ] || wget https://raw.githubusercontent.com/microverseinc/linters-config/master/ror/.stylelintrc.json
+ - name: Stylelint Report
+ run: npx stylelint "**/*.{css,scss}"
+ nodechecker:
+ name: node_modules checker
+ runs-on: ubuntu-22.04
+ steps:
+ - uses: actions/checkout@v3
+ - name: Check node_modules existence
+ run: |
+ if [ -d "node_modules/" ]; then echo -e "\e[1;31mThe node_modules/ folder was pushed to the repo. Please remove it from the GitHub repository and try again."; echo -e "\e[1;32mYou can set up a .gitignore file with this folder included on it to prevent this from happening in the future." && exit 1; fi
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e16dc71..e72bec7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,6 @@
# Ignore master key for decrypting credentials and more.
/config/master.key
+
+# .gitignore
+node_modules/
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..07baeb4
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,60 @@
+AllCops:
+ NewCops: enable
+ Exclude:
+ - "db/**/*"
+ - "bin/*"
+ - "config/**/*"
+ - "Guardfile"
+ - "Rakefile"
+ - "node_modules/**/*"
+
+ DisplayCopNames: true
+
+Layout/LineLength:
+ Max: 120
+Metrics/MethodLength:
+ Include:
+ - "app/controllers/*"
+ - "app/models/*"
+ Max: 20
+Metrics/AbcSize:
+ Include:
+ - "app/controllers/*"
+ - "app/models/*"
+ Max: 50
+Metrics/ClassLength:
+ Max: 150
+Metrics/BlockLength:
+ AllowedMethods: ['describe']
+ Max: 30
+
+Style/Documentation:
+ Enabled: false
+Style/ClassAndModuleChildren:
+ Enabled: false
+Style/EachForSimpleLoop:
+ Enabled: false
+Style/AndOr:
+ Enabled: false
+Style/DefWithParentheses:
+ Enabled: false
+Style/FrozenStringLiteralComment:
+ EnforcedStyle: never
+
+Layout/HashAlignment:
+ EnforcedColonStyle: key
+Layout/ExtraSpacing:
+ AllowForAlignment: false
+Layout/MultilineMethodCallIndentation:
+ Enabled: true
+ EnforcedStyle: indented
+Lint/RaiseException:
+ Enabled: false
+Lint/StructNewOverride:
+ Enabled: false
+Style/HashEachMethods:
+ Enabled: false
+Style/HashTransformKeys:
+ Enabled: false
+Style/HashTransformValues:
+ Enabled: false
\ No newline at end of file
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 0000000..dd5d140
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,37 @@
+{
+ "extends": ["stylelint-config-standard"],
+ "plugins": ["stylelint-scss", "stylelint-csstree-validator"],
+ "rules": {
+ "at-rule-no-unknown": [
+ true,
+ {
+ "ignoreAtRules": [
+ "tailwind",
+ "apply",
+ "variants",
+ "responsive",
+ "screen"
+ ]
+ }
+ ],
+ "scss/at-rule-no-unknown": [
+ true,
+ {
+ "ignoreAtRules": [
+ "tailwind",
+ "apply",
+ "variants",
+ "responsive",
+ "screen"
+ ]
+ }
+ ],
+ "csstree/validator": true
+ },
+ "ignoreFiles": [
+ "build/**",
+ "dist/**",
+ "**/reset*.css",
+ "**/bootstrap*.css"
+ ]
+}
diff --git a/Gemfile b/Gemfile
index b559af4..ed02a96 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,31 +1,31 @@
-source "https://rubygems.org"
+source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
-ruby "3.2.2"
+ruby '3.2.2'
# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
-gem "rails", "~> 7.0.8"
+gem 'rails', '~> 7.0.8'
# The original asset pipeline for Rails [https://github.com/rails/sprockets-rails]
-gem "sprockets-rails"
+gem 'sprockets-rails'
# Use postgresql as the database for Active Record
-gem "pg", "~> 1.1"
+gem 'pg', '~> 1.1'
# Use the Puma web server [https://github.com/puma/puma]
-gem "puma", "~> 5.0"
+gem 'puma', '~> 5.0'
# Use JavaScript with ESM import maps [https://github.com/rails/importmap-rails]
-gem "importmap-rails"
+gem 'importmap-rails'
# Hotwire's SPA-like page accelerator [https://turbo.hotwired.dev]
-gem "turbo-rails"
+gem 'turbo-rails'
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
-gem "stimulus-rails"
+gem 'stimulus-rails'
# Build JSON APIs with ease [https://github.com/rails/jbuilder]
-gem "jbuilder"
+gem 'jbuilder'
# Use Redis adapter to run Action Cable in production
# gem "redis", "~> 4.0"
@@ -37,10 +37,10 @@ gem "jbuilder"
# 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 ]
+gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
# Reduces boot times through caching; required in config/boot.rb
-gem "bootsnap", require: false
+gem 'bootsnap', require: false
# Use Sass to process CSS
# gem "sassc-rails"
@@ -50,12 +50,15 @@ gem "bootsnap", require: false
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 'debug', platforms: %i[mri mingw x64_mingw]
+
+ # Rspec
+ gem 'rspec-rails'
end
group :development do
# Use console on exceptions pages [https://github.com/rails/web-console]
- gem "web-console"
+ gem 'web-console'
# Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
# gem "rack-mini-profiler"
@@ -66,7 +69,10 @@ end
group :test do
# Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
- gem "capybara"
- gem "selenium-webdriver"
-
+ gem 'capybara'
+ gem 'selenium-webdriver'
end
+
+gem 'devise', '~> 4.9'
+
+gem 'rubocop', '>= 1.0', '< 2.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 700b38f..03b2123 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -68,6 +68,9 @@ GEM
tzinfo (~> 2.0)
addressable (2.8.5)
public_suffix (>= 2.0.2, < 6.0)
+ ast (2.4.2)
+ base64 (0.1.1)
+ bcrypt (3.1.19)
bindex (0.8.1)
bootsnap (1.16.0)
msgpack (~> 1.2)
@@ -87,6 +90,13 @@ GEM
debug (1.8.0)
irb (>= 1.5.0)
reline (>= 0.3.1)
+ devise (4.9.3)
+ bcrypt (~> 3.0)
+ orm_adapter (~> 0.1)
+ railties (>= 4.1.0)
+ responders
+ warden (~> 1.2.3)
+ diff-lcs (1.5.0)
erubi (1.12.0)
globalid (1.2.1)
activesupport (>= 6.1)
@@ -102,6 +112,8 @@ GEM
jbuilder (2.11.5)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
+ json (2.6.3)
+ language_server-protocol (3.17.0.3)
loofah (2.21.4)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
@@ -128,6 +140,14 @@ GEM
nio4r (2.5.9)
nokogiri (1.15.4-x64-mingw-ucrt)
racc (~> 1.4)
+ nokogiri (1.15.4-x86_64-linux)
+ racc (~> 1.4)
+ orm_adapter (0.5.0)
+ parallel (1.23.0)
+ parser (3.2.2.4)
+ ast (~> 2.4.1)
+ racc
+ pg (1.5.4)
pg (1.5.4-x64-mingw-ucrt)
psych (5.1.1.1)
stringio
@@ -166,13 +186,49 @@ GEM
rake (>= 12.2)
thor (~> 1.0)
zeitwerk (~> 2.5)
+ rainbow (3.1.1)
rake (13.0.6)
rdoc (6.5.0)
psych (>= 4.0.0)
regexp_parser (2.8.2)
reline (0.3.9)
io-console (~> 0.5)
+ responders (3.1.1)
+ actionpack (>= 5.2)
+ railties (>= 5.2)
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.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.56.4)
+ base64 (~> 0.1.1)
+ json (~> 2.3)
+ language_server-protocol (>= 3.17.0)
+ parallel (~> 1.10)
+ parser (>= 3.2.2.3)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
+ rexml (>= 3.2.5, < 4.0)
+ rubocop-ast (>= 1.28.1, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 3.0)
+ rubocop-ast (1.29.0)
+ parser (>= 3.2.1.0)
+ ruby-progressbar (1.13.0)
rubyzip (2.3.2)
selenium-webdriver (4.14.0)
rexml (~> 3.2, >= 3.2.5)
@@ -198,6 +254,9 @@ GEM
concurrent-ruby (~> 1.0)
tzinfo-data (1.2023.3)
tzinfo (>= 1.0.0)
+ unicode-display_width (2.5.0)
+ warden (1.2.9)
+ rack (>= 2.0.9)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
@@ -213,16 +272,20 @@ GEM
PLATFORMS
x64-mingw-ucrt
+ x86_64-linux
DEPENDENCIES
bootsnap
capybara
debug
+ devise (~> 4.9)
importmap-rails
jbuilder
pg (~> 1.1)
puma (~> 5.0)
rails (~> 7.0.8)
+ rspec-rails
+ rubocop (>= 1.0, < 2.0)
selenium-webdriver
sprockets-rails
stimulus-rails
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..b8f260c
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 HassanShakur
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 7db80e4..cbad11f 100644
--- a/README.md
+++ b/README.md
@@ -1,24 +1,173 @@
-# README
+
-This README would normally document whatever steps are necessary to get the
-application up and running.
+
-Things you may want to cover:
+# 📗 Table of Contents
-* Ruby version
+- [📗 Table of Contents](#-table-of-contents)
+- [📖 \[budget-app\] ](#-budget-app-)
+ - [🛠 Built With ](#-built-with-)
+ - [Tech Stack ](#tech-stack-)
+ - [Key Features ](#key-features-)
+ - [🚀 Live Demo ](#-live-demo-)
+ - [💻 Getting Started ](#-getting-started-)
+ - [Prerequisites](#prerequisites)
+ - [Setup](#setup)
+ - [👥 Authors ](#-authors-)
+ - [🔭 Future Features ](#-future-features-)
+ - [🤝 Contributing ](#-contributing-)
+ - [⭐️ Show your support ](#️-show-your-support-)
+ - [🙏 Acknowledgments ](#-acknowledgments-)
+ - [📝 License ](#-license-)
-* System dependencies
+
-* Configuration
+# 📖 [budget-app]
-* Database creation
+**[budget-app]** is a Ruby on Rails application that allows users to create budgets and track their expenses.
-* Database initialization
+## 🛠 Built With
-* How to run the test suite
+### Tech Stack
-* Services (job queues, cache servers, search engines, etc.)
+
+Ruby
+
+
-* Deployment instructions
+
+Rails
+
+
-* ...
+
+Postgresql
+
+
+
+
+
+### Key Features
+
+- **Create new transaction groups.**
+- **Create new transactions.**
+- **View all transactions.**
+- **View all transaction groups.**
+- **View all transactions for a specific group.**
+
+
(back to top)
+
+
+
+## 🚀 Live Demo
+
+ See Project 🚀
+
+ Presentation video
+
+
+
+## 💻 Getting Started
+
+To get a local copy up and running, follow these steps.
+
+### Prerequisites
+
+In order to run this project you need to have `ruby`, `rails` and `postgres` installed.
+
+### Setup
+
+Clone this repository to your desired folder:
+
+```sh
+ cd my-folder
+ git clone github.com/microhass/budget-app.git
+```
+
+Then navigate to `budget-app` folder:
+
+```sh
+cd budget-app
+```
+
+Install necessary dependencies:
+
+```sh
+bundle install
+```
+
+Then start the app server:
+
+```sh
+rails serve
+```
+
+Visit [http://127.0.0.1:3000/](localhost) to view the app.
+
+
+(back to top)
+
+
+
+## 👥 Authors
+
+👤 **Hassan Shakur**
+
+- GitHub: [@hassanShakur](https://github.com/hassanShakur)
+- Twitter: [@HassShakur](https://twitter.com/HassShakur)
+- LinkedIn: [hassanShakur](https://linkedin.com/in/hassanShakur)
+
+(back to top)
+
+
+
+## 🔭 Future Features
+
+- [ ] **Implement the desktop version of the app.**
+- [ ] **Improve on the mobile UI and UX.**
+- [ ] **Add more features to the app.**
+- [ ] **Add more tests.**
+
+(back to top)
+
+
+
+## 🤝 Contributing
+
+Contributions, issues, and feature requests are welcome!
+
+Feel free to check the [issues page](../../issues/).
+
+(back to top)
+
+
+
+## ⭐️ Show your support
+
+> If you'd like to contribute to this project, feel free to fork the repository and make changes as you see fit. Please submit a pull request with your changes and I'll review them as soon as possible.
+
+(back to top)
+
+
+
+## 🙏 Acknowledgments
+
+> I would like to thank my coding partners and mentor for the support, and Microverse for the opportunity.
+>
+> Special thanks to [Gregoire Vella on Behance](https://www.behance.net/gregoirevella) for the design inspiration.
+
+(back to top)
+
+
+
+## 📝 License
+
+This project is [MIT](./LICENSE) licensed.
+
+(back to top)
diff --git a/app/assets/fonts/proximanova-regular.otf b/app/assets/fonts/proximanova-regular.otf
new file mode 100644
index 0000000..8aca013
Binary files /dev/null and b/app/assets/fonts/proximanova-regular.otf differ
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 288b9ab..6eee397 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -13,3 +13,250 @@
*= require_tree .
*= require_self
*/
+
+@font-face {
+ font-family: 'Proxima Nova';
+ src: url('proximanova-regular.otf') format('opentype');
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+ transition: all 0.3s ease-in-out;
+ font-family: 'Proxima Nova', sans-serif;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+body {
+ background-color: #2b2b2b;
+}
+
+main {
+ max-width: 500px;
+ margin: 0 auto;
+ padding-bottom: 2rem;
+ font-family: 'Roboto', sans-serif;
+ font-size: 16px;
+ color: #434b54;
+ min-height: 740px;
+ margin-top: 2vh;
+ border-radius: 5px;
+ background-color: #fff;
+}
+
+#notice,
+#alert {
+ text-align: center;
+ padding: 0.5rem;
+ background-color: #3778c2;
+ color: #fff;
+}
+
+.header {
+ background-color: #3778c2;
+ color: #fff;
+ padding: 1rem 0.6rem;
+}
+
+.header h1 {
+ font-size: 1.4rem;
+}
+
+.grid-display {
+ display: grid;
+}
+
+#splash-screen {
+ width: 100%;
+ height: 80vh;
+ grid-template-rows: 3fr 1fr;
+}
+
+#splash-screen h1 {
+ place-items: center;
+}
+
+#splash-links {
+ gap: 1rem;
+ padding: 10%;
+}
+
+#user-info {
+ background-color: #3778c2;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 0.5rem;
+ border-bottom: 1px solid #fff;
+}
+
+.btn {
+ display: grid;
+ place-items: center;
+ border-radius: 3px;
+ padding: 0.8rem;
+ width: 90%;
+ margin: 0 auto;
+ margin-top: 10px;
+}
+
+#user-info p {
+ color: #fff;
+ font-size: 0.8rem;
+}
+
+#user-info .btn {
+ background-color: red;
+ color: #fff;
+ width: fit-content;
+ padding: 0.7rem 2rem;
+ margin-bottom: 15px;
+}
+
+#user-info .btn:hover {
+ background-color: #fa3131;
+ cursor: pointer;
+}
+
+.btn.pri,
+input[type='submit'] {
+ background-color: #3778c2;
+ color: #fff;
+ padding: 0.8rem;
+ margin: 10px auto;
+}
+
+.btn.pri:hover {
+ background-color: #5b8cc4;
+}
+
+.btn.sec {
+ background-color: transparent;
+ color: #434b54 !important;
+}
+
+.btn.sec:hover {
+ background-color: #ededed;
+}
+
+.btn.action {
+ background-color: #5fb523;
+ color: #fff;
+}
+
+.btn.action:hover {
+ background-color: #78b74b;
+}
+
+#splash-links a {
+ height: 3rem;
+}
+
+#form {
+ display: grid;
+ padding: 1rem 4%;
+}
+
+#form > a,
+#back {
+ text-align: center;
+ margin-top: 10px;
+ color: #434b546a;
+}
+
+#back {
+ margin-top: 20px;
+}
+
+#form h2 {
+ text-align: center;
+ padding: 1rem;
+ margin-bottom: 2rem;
+}
+
+#form form {
+ gap: 1rem;
+}
+
+#form form,
+form > div {
+ display: grid;
+}
+
+input,
+button {
+ outline: none;
+ border: none;
+ padding: 0.8rem 0;
+}
+
+.field input {
+ border-bottom: 1px solid #2b2b2b76;
+ font-size: 14px;
+}
+
+.actions {
+ margin-top: 2rem;
+}
+
+#groups {
+ gap: 1rem;
+ padding: 1rem;
+ background-color: #f5f5f5;
+}
+
+#group {
+ grid-template-columns: 1fr 3fr 1fr;
+ gap: 1.4rem;
+ padding: 0.6rem;
+ background-color: #fff;
+ border-radius: 5px;
+}
+
+#group:hover {
+ background-color: #d1d1d190;
+}
+
+#group .img-div {
+ width: 100%;
+ height: 80px;
+ overflow: hidden;
+ border-radius: 2px;
+}
+
+#group img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+}
+
+.group-info {
+ justify-content: space-between;
+}
+
+#transactions {
+ gap: 1rem;
+ margin-top: 3rem;
+}
+
+#entity {
+ gap: 1rem;
+ padding: 0.6rem;
+ border-bottom: 1px solid #2b2b2b76;
+}
+
+#entity > div {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+#date {
+ opacity: 0.5;
+ font-size: 12px;
+}
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 09705d1..7b2e7be 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,2 +1,21 @@
class ApplicationController < ActionController::Base
+ protect_from_forgery with: :exception
+ before_action :update_allowed_parameters, if: :devise_controller?
+
+ def after_sign_in_path_for(_resource)
+ authenticated_root_path
+ end
+
+ def after_sign_out_path_for(_resource)
+ unauthenticated_root_path
+ end
+
+ protected
+
+ def update_allowed_parameters
+ devise_parameter_sanitizer.permit(:sign_up) { |u| u.permit(:name, :email, :password) }
+ devise_parameter_sanitizer.permit(:account_update) do |u|
+ u.permit(:name, :email, :password, :current_password)
+ end
+ end
end
diff --git a/app/controllers/entities_controller.rb b/app/controllers/entities_controller.rb
new file mode 100644
index 0000000..d500046
--- /dev/null
+++ b/app/controllers/entities_controller.rb
@@ -0,0 +1,36 @@
+class EntitiesController < ApplicationController
+ before_action :authenticate_user!
+ before_action :set_group, only: %i[new create]
+ before_action :set_entity, only: %i[show edit update destroy]
+
+ def new
+ @entity = Entity.new
+ end
+
+ def create
+ @entity = Entity.new(entity_params)
+ @entity.author_id = current_user.id
+
+ respond_to do |format|
+ if @entity.save
+ format.html { redirect_to group_path(@group), notice: 'Entity was successfully created.' }
+ else
+ format.html { render :new, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ private
+
+ def set_entity
+ @entity = Entity.find(params[:id])
+ end
+
+ def set_group
+ @group = Group.find(params[:group_id])
+ end
+
+ def entity_params
+ params.require(:entity).permit(:name, :amount, group_ids: [])
+ end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
new file mode 100644
index 0000000..99b3706
--- /dev/null
+++ b/app/controllers/groups_controller.rb
@@ -0,0 +1,60 @@
+class GroupsController < ApplicationController
+ before_action :authenticate_user!
+ before_action :set_group, only: %i[show edit update destroy]
+
+ def index
+ # show only groups that belong to the current user
+ @groups = current_user.groups
+ end
+
+ def show
+ @transactions = @group.entities
+ end
+
+ def new
+ @group = Group.new
+ end
+
+ def edit; end
+
+ def create
+ @group = Group.new(group_params)
+ @group.user = current_user
+
+ respond_to do |format|
+ if @group.save
+ format.html { redirect_to groups_url, notice: 'Group was successfully created.' }
+ else
+ format.html { render :new, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ def update
+ respond_to do |format|
+ if @group.update(group_params)
+ format.html { redirect_to group_url(@group), notice: 'Group was successfully updated.' }
+ else
+ format.html { render :edit, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ def destroy
+ @group.destroy
+
+ respond_to do |format|
+ format.html { redirect_to groups_url, notice: 'Group was successfully destroyed.' }
+ end
+ end
+
+ private
+
+ def set_group
+ @group = Group.find(params[:id])
+ end
+
+ def group_params
+ params.require(:group).permit(:name, :icon)
+ end
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
new file mode 100644
index 0000000..7b17fd4
--- /dev/null
+++ b/app/controllers/users_controller.rb
@@ -0,0 +1,58 @@
+class UsersController < ApplicationController
+ before_action :set_user, only: %i[show edit update destroy]
+
+ # GET /users/new
+ def new
+ @user = User.new
+ end
+
+ # POST /users or /users.json
+ def create
+ @user = User.new(user_params)
+
+ respond_to do |format|
+ if @user.save
+ format.html { redirect_to user_url(@user), notice: 'User was successfully created.' }
+ format.json { render :show, status: :created, location: @user }
+ else
+ format.html { render :new, status: :unprocessable_entity }
+ format.json { render json: @user.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # PATCH/PUT /users/1 or /users/1.json
+ def update
+ respond_to do |format|
+ if @user.update(user_params)
+ format.html { redirect_to user_url(@user), notice: 'User was successfully updated.' }
+ format.json { render :show, status: :ok, location: @user }
+ else
+ format.html { render :edit, status: :unprocessable_entity }
+ format.json { render json: @user.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+
+ # DELETE /users/1 or /users/1.json
+ def destroy
+ @user.destroy
+
+ respond_to do |format|
+ format.html { redirect_to users_url, notice: 'User was successfully destroyed.' }
+ format.json { head :no_content }
+ end
+ end
+
+ private
+
+ # Use callbacks to share common setup or constraints between actions.
+ def set_user
+ @user = User.find(params[:id])
+ end
+
+ # Only allow a list of trusted parameters through.
+ def user_params
+ params.require(:user).permit(:name)
+ end
+end
diff --git a/app/helpers/entities_helper.rb b/app/helpers/entities_helper.rb
new file mode 100644
index 0000000..7437498
--- /dev/null
+++ b/app/helpers/entities_helper.rb
@@ -0,0 +1,2 @@
+module EntitiesHelper
+end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
new file mode 100644
index 0000000..c091b2f
--- /dev/null
+++ b/app/helpers/groups_helper.rb
@@ -0,0 +1,2 @@
+module GroupsHelper
+end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
new file mode 100644
index 0000000..2310a24
--- /dev/null
+++ b/app/helpers/users_helper.rb
@@ -0,0 +1,2 @@
+module UsersHelper
+end
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
deleted file mode 100644
index d394c3d..0000000
--- a/app/jobs/application_job.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-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/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/entity.rb b/app/models/entity.rb
new file mode 100644
index 0000000..1dad8f5
--- /dev/null
+++ b/app/models/entity.rb
@@ -0,0 +1,6 @@
+class Entity < ApplicationRecord
+ validates :name, presence: true
+ validates :groups, presence: true
+ validates :amount, presence: true, numericality: { greater_than: 0 }
+ has_and_belongs_to_many :groups
+end
diff --git a/app/models/group.rb b/app/models/group.rb
new file mode 100644
index 0000000..486a5aa
--- /dev/null
+++ b/app/models/group.rb
@@ -0,0 +1,6 @@
+class Group < ApplicationRecord
+ validates :name, presence: true
+ validates :icon, presence: true
+ belongs_to :user
+ has_and_belongs_to_many :entities
+end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..0c8328b
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,10 @@
+class User < ApplicationRecord
+ # Include default devise modules. Others available are:
+ # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
+ devise :database_authenticatable, :registerable,
+ :recoverable, :rememberable, :validatable
+
+ validates :name, presence: true
+ has_many :entities, dependent: :destroy
+ has_many :groups, dependent: :destroy
+end
diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb
new file mode 100644
index 0000000..b12dd0c
--- /dev/null
+++ b/app/views/devise/confirmations/new.html.erb
@@ -0,0 +1,16 @@
+Resend confirmation instructions
+
+<%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
+
+
+
+ <%= f.submit "Resend confirmation instructions" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb
new file mode 100644
index 0000000..dc55f64
--- /dev/null
+++ b/app/views/devise/mailer/confirmation_instructions.html.erb
@@ -0,0 +1,5 @@
+Welcome <%= @email %>!
+
+You can confirm your account email through the link below:
+
+<%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>
diff --git a/app/views/devise/mailer/email_changed.html.erb b/app/views/devise/mailer/email_changed.html.erb
new file mode 100644
index 0000000..32f4ba8
--- /dev/null
+++ b/app/views/devise/mailer/email_changed.html.erb
@@ -0,0 +1,7 @@
+Hello <%= @email %>!
+
+<% if @resource.try(:unconfirmed_email?) %>
+ We're contacting you to notify you that your email is being changed to <%= @resource.unconfirmed_email %>.
+<% else %>
+ We're contacting you to notify you that your email has been changed to <%= @resource.email %>.
+<% end %>
diff --git a/app/views/devise/mailer/password_change.html.erb b/app/views/devise/mailer/password_change.html.erb
new file mode 100644
index 0000000..b41daf4
--- /dev/null
+++ b/app/views/devise/mailer/password_change.html.erb
@@ -0,0 +1,3 @@
+Hello <%= @resource.email %>!
+
+We're contacting you to notify you that your password has been changed.
diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb
new file mode 100644
index 0000000..f667dc1
--- /dev/null
+++ b/app/views/devise/mailer/reset_password_instructions.html.erb
@@ -0,0 +1,8 @@
+Hello <%= @resource.email %>!
+
+Someone has requested a link to change your password. You can do this through the link below.
+
+<%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>
+
+If you didn't request this, please ignore this email.
+Your password won't change until you access the link above and create a new one.
diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb
new file mode 100644
index 0000000..41e148b
--- /dev/null
+++ b/app/views/devise/mailer/unlock_instructions.html.erb
@@ -0,0 +1,7 @@
+Hello <%= @resource.email %>!
+
+Your account has been locked due to an excessive number of unsuccessful sign in attempts.
+
+Click the link below to unlock your account:
+
+<%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>
diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb
new file mode 100644
index 0000000..5fbb9ff
--- /dev/null
+++ b/app/views/devise/passwords/edit.html.erb
@@ -0,0 +1,25 @@
+Change your password
+
+<%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+ <%= f.hidden_field :reset_password_token %>
+
+
+ <%= f.label :password, "New password" %>
+ <% if @minimum_password_length %>
+ (<%= @minimum_password_length %> characters minimum)
+ <% end %>
+ <%= f.password_field :password, autofocus: true, autocomplete: "new-password" %>
+
+
+
+ <%= f.label :password_confirmation, "Confirm new password" %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
+
+
+
+ <%= f.submit "Change my password" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb
new file mode 100644
index 0000000..0f61a3c
--- /dev/null
+++ b/app/views/devise/passwords/new.html.erb
@@ -0,0 +1,17 @@
+
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb
new file mode 100644
index 0000000..588f9d9
--- /dev/null
+++ b/app/views/devise/registrations/edit.html.erb
@@ -0,0 +1,48 @@
+
+
Edit <%= resource_name.to_s.humanize %>
+
+ <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.text_field :name, autofocus: true, autocomplete: "name", placeholder: 'Full Name' %>
+
+
+
+ <%= f.email_field :email, autocomplete: "email", placeholder: 'Email' %>
+
+
+ <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %>
+
Currently waiting confirmation for: <%= resource.unconfirmed_email %>
+ <% end %>
+
+
+ <%= f.label :password %> (leave blank if you don't want to change it)
+ <%= f.password_field :password, autocomplete: "new-password" %>
+ <% if @minimum_password_length %>
+
+ <%= @minimum_password_length %> characters minimum
+ <% end %>
+
+
+
+ <%= f.label :password_confirmation %>
+ <%= f.password_field :password_confirmation, autocomplete: "new-password" %>
+
+
+
+ <%= f.label :current_password %> (we need your )
+ <%= f.password_field :current_password, autocomplete: "current-password", placeholder: 'Enter current password to confirm your changes' %>
+
+
+
+ <%= f.submit "Update" %>
+
+ <% end %>
+
+
Cancel my account
+
+
Unhappy? <%= button_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" }, method: :delete %>
+
+ <%= link_to "Back", :back %>
+
diff --git a/app/views/devise/registrations/new.html.erb b/app/views/devise/registrations/new.html.erb
new file mode 100644
index 0000000..9e8bd2a
--- /dev/null
+++ b/app/views/devise/registrations/new.html.erb
@@ -0,0 +1,29 @@
+
diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb
new file mode 100644
index 0000000..2c1225f
--- /dev/null
+++ b/app/views/devise/sessions/new.html.erb
@@ -0,0 +1,18 @@
+
\ No newline at end of file
diff --git a/app/views/devise/shared/_error_messages.html.erb b/app/views/devise/shared/_error_messages.html.erb
new file mode 100644
index 0000000..cabfe30
--- /dev/null
+++ b/app/views/devise/shared/_error_messages.html.erb
@@ -0,0 +1,15 @@
+<% if resource.errors.any? %>
+
+
+ <%= I18n.t("errors.messages.not_saved",
+ count: resource.errors.count,
+ resource: resource.class.model_name.human.downcase)
+ %>
+
+
+ <% resource.errors.full_messages.each do |message| %>
+ - <%= message %>
+ <% end %>
+
+
+<% end %>
diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb
new file mode 100644
index 0000000..39fafb6
--- /dev/null
+++ b/app/views/devise/shared/_links.html.erb
@@ -0,0 +1,25 @@
+<%- if controller_name != 'sessions' %>
+ <%= link_to "Log in", new_session_path(resource_name), class: 'sec btn' %>
+<% end %>
+
+<%- if devise_mapping.registerable? && controller_name != 'registrations' %>
+ <%= link_to "Sign up", new_registration_path(resource_name), class: 'sec btn' %>
+<% end %>
+
+<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %>
+ <%= link_to "Forgot your password?", new_password_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %>
+ <%= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %>
+ <%= link_to "Didn't receive unlock instructions?", new_unlock_path(resource_name) %>
+<% end %>
+
+<%- if devise_mapping.omniauthable? %>
+ <%- resource_class.omniauth_providers.each do |provider| %>
+ <%= button_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider), data: { turbo: false } %>
+ <% end %>
+<% end %>
diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb
new file mode 100644
index 0000000..ffc34de
--- /dev/null
+++ b/app/views/devise/unlocks/new.html.erb
@@ -0,0 +1,16 @@
+Resend unlock instructions
+
+<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
+ <%= render "devise/shared/error_messages", resource: resource %>
+
+
+ <%= f.label :email %>
+ <%= f.email_field :email, autofocus: true, autocomplete: "email" %>
+
+
+
+ <%= f.submit "Resend unlock instructions" %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/entities/_entity.html.erb b/app/views/entities/_entity.html.erb
new file mode 100644
index 0000000..7da21d3
--- /dev/null
+++ b/app/views/entities/_entity.html.erb
@@ -0,0 +1,11 @@
+
+
+
Transaction: <%= entity.name %>
+
$<%= entity.amount %>
+
+
+
+ <%= entity.created_at.strftime("%d %b %Y") %>
+ at <%= entity.created_at.strftime("%I:%M %p") %>
+
+
diff --git a/app/views/entities/_form.html.erb b/app/views/entities/_form.html.erb
new file mode 100644
index 0000000..f4e9466
--- /dev/null
+++ b/app/views/entities/_form.html.erb
@@ -0,0 +1,30 @@
+<%= form_with model: entity, url: group_entities_path(@group), local: true do |form| %>
+ <% if entity.errors.any? %>
+
+
<%= pluralize(entity.errors.count, "error") %> prohibited this entity from being saved:
+
+
+ <% entity.errors.each do |error| %>
+ - <%= error.full_message %>
+ <% end %>
+
+
+ <% end %>
+
+
+ <%= form.text_field :name, placeholder: 'Name' %>
+
+
+
+ <%= form.text_field :amount, placeholder: 'Amount' %>
+
+
+
+ <%= form.label :group_ids, 'Groups (Hold Ctrl & select)' %>
+ <%= form.collection_select :group_ids, current_user.groups.all, :id, :name, {prompt: 'Select Group'}, {multiple: true} %>
+
+
+
+ <%= form.submit %>
+
+<% end %>
diff --git a/app/views/entities/new.html.erb b/app/views/entities/new.html.erb
new file mode 100644
index 0000000..65c9e4f
--- /dev/null
+++ b/app/views/entities/new.html.erb
@@ -0,0 +1,11 @@
+
diff --git a/app/views/entities/show.json.jbuilder b/app/views/entities/show.json.jbuilder
new file mode 100644
index 0000000..0337c8d
--- /dev/null
+++ b/app/views/entities/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'entities/entity', entity: @entity
diff --git a/app/views/groups/_form.html.erb b/app/views/groups/_form.html.erb
new file mode 100644
index 0000000..2b0dd23
--- /dev/null
+++ b/app/views/groups/_form.html.erb
@@ -0,0 +1,25 @@
+<%= form_with(model: group) do |form| %>
+ <% if group.errors.any? %>
+
+
<%= pluralize(group.errors.count, "error") %> prohibited this group from being saved:
+
+
+ <% group.errors.each do |error| %>
+ - <%= error.full_message %>
+ <% end %>
+
+
+ <% end %>
+
+
+ <%= form.text_field :name, placeholder: 'Name' %>
+
+
+
+ <%= form.text_field :icon, placeholder: 'Icon' %>
+
+
+
+ <%= form.submit %>
+
+<% end %>
diff --git a/app/views/groups/_group.html.erb b/app/views/groups/_group.html.erb
new file mode 100644
index 0000000..357c463
--- /dev/null
+++ b/app/views/groups/_group.html.erb
@@ -0,0 +1,17 @@
+<%= link_to group_path(group) do %>
+
+
+ <%= image_tag group.icon %>
+
+
+
+
<%= group.name %>
+
<%= group.created_at.strftime("%d %b %Y") %>
+
+
+
+ $<%= group.entities.sum(:amount) %>
+
+
+
+<% end %>
diff --git a/app/views/groups/index.html.erb b/app/views/groups/index.html.erb
new file mode 100644
index 0000000..c2d7d69
--- /dev/null
+++ b/app/views/groups/index.html.erb
@@ -0,0 +1,11 @@
+
+
+
+
+ <%= render @groups %>
+
+
+ <%= link_to "Add new category", new_group_path, class: 'action btn' %>
+
diff --git a/app/views/groups/new.html.erb b/app/views/groups/new.html.erb
new file mode 100644
index 0000000..3b50ca1
--- /dev/null
+++ b/app/views/groups/new.html.erb
@@ -0,0 +1,11 @@
+
diff --git a/app/views/groups/show.html.erb b/app/views/groups/show.html.erb
new file mode 100644
index 0000000..37368b4
--- /dev/null
+++ b/app/views/groups/show.html.erb
@@ -0,0 +1,25 @@
+
+
+
+
+ <%= image_tag @group.icon %>
+
+
+
+
<%= @group.name %>
+
+ Total Transaction Amount: $<%= @group.entities.sum(:amount) %>
+
+
+
+
+
+ <%= render partial: 'entities/entity', collection: @group.entities.order(created_at: :desc) %>
+
+ <%= link_to "Add a new transaction", new_group_entity_path(@group), class: 'action btn' %>
+
+
+
+
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 03fc2f6..584ba32 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -1,7 +1,7 @@
- BudgetApp
+ Budgetize
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
@@ -11,6 +11,16 @@
- <%= yield %>
+
+ <%= notice %>
+ <%= alert %>
+ <% if user_signed_in? then %>
+
+
Logged in as <%= current_user.email %>
+ <%= button_to "Logout" , destroy_user_session_path, method: :delete, class: 'btn' %>
+
+ <% end %>
+ <%= yield %>
+
diff --git a/app/views/users/index.json.jbuilder b/app/views/users/index.json.jbuilder
new file mode 100644
index 0000000..2faf5af
--- /dev/null
+++ b/app/views/users/index.json.jbuilder
@@ -0,0 +1 @@
+json.array! @users, partial: 'users/user', as: :user
diff --git a/app/views/users/show.json.jbuilder b/app/views/users/show.json.jbuilder
new file mode 100644
index 0000000..2a33f71
--- /dev/null
+++ b/app/views/users/show.json.jbuilder
@@ -0,0 +1 @@
+json.partial! 'users/user', user: @user
diff --git a/app/views/users/splash.html.erb b/app/views/users/splash.html.erb
new file mode 100644
index 0000000..56dfb05
--- /dev/null
+++ b/app/views/users/splash.html.erb
@@ -0,0 +1,8 @@
+
+
Budgetize
+
+
+ <%= link_to "Login", new_user_session_path, class: 'pri btn' %>
+ <%= link_to "Sign Up", new_user_registration_path, class: 'sec btn' %>
+
+
diff --git a/bin/render-build.sh b/bin/render-build.sh
new file mode 100644
index 0000000..62f25e4
--- /dev/null
+++ b/bin/render-build.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# exit on error
+set -o errexit
+
+bundle install
+bundle exec rails assets:precompile
+bundle exec rails assets:clean
+bundle exec rails db:migrate
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/credentials.yml.enc b/config/credentials.yml.enc
index 29c93ab..7f51218 100644
--- a/config/credentials.yml.enc
+++ b/config/credentials.yml.enc
@@ -1 +1 @@
-SU1tTMu1Hu5lEYlXMS68WGsqlpJ3rR+cgeqTSQFoZyhflVKcde3JlM9hYlHwTBxx8Gcs3yBaKeziLCuPOKgiDpMmN4xqE12zFzptpLmOA/sCF9quBps4N+N/3BcCK7PezOVn2SvjdRWn8wCXauq5TJq3HCH5LGTtQ78ua34yW4lQFCM1HUE7EPxSqGZ63vOQABj2LUlkFCP6w5VbrQN+YBhWm9rO5IiSsKBTwkW1hkTUAFu9ilL46J6NZjgtfXqG9OtmZp8dfHNNRy75GWsqYAhK5pinZBTqV3gK5PIA38A9LPdNXnf8EDE3D6GG+qwXctbUO4cn16xLUqEouW/N3Wt0AHJhI1slGtSYiBflhewTiSGP7rRV3iP164ol2TKRHH/Xc+xE+OzgcrV2RuMbfLlfmR9Pe9i0CKNX--5R8tK0derlzWe+aR--6uVVqaSiNaPek779TRaPbA==
\ No newline at end of file
+51mHTcxxzTU9kGBGMzy6q9Kpmhtkp+lZPmJqTCeFhQgmUPk2DKmjFzYr+trU+K/kt5t2ulTjoXN35w8omJVKlA4w5WdLak2oerUV2sly22Rqd6GmAbAL8QKX/956XvMG4Ot3fZ0RLWZ5oJoVTQUbjTZljItPdOggh4AZx8U+s7QakFv7Ks5015f1RJyfE1wNlGsMMRCJzG3hN/BQSiUWzUc6cttMwXWsnTnzdoeDSC4SwAqHYs4avFHslWKbFxcDDga9moRI5kL87KZL7CBs92ZUJ0QvVIbklSEWylz6POZqg9kxDCHmgCaQFlptvD/tn71sG/r9OYJcMcyfKhRZX7I3hpcU+WwtgPMLlsECNqKjSHjAKt36fe4r5Qq0Eo30OfJ5G0oaT+999xJDubRX9l+3KOsHhl8n0YU7--qOTftRs//feWYT4E--/vJRwrnyNZ7jStDQhdzAwA==
\ No newline at end of file
diff --git a/config/database.yml b/config/database.yml
index 32624f6..afb9dd1 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -24,6 +24,8 @@ default: &default
development:
<<: *default
database: budget_app_development
+ username: Dev
+ password: 12345678
# The specified database role being used to connect to postgres.
# To create additional roles in postgres see `$ createuser --help`.
@@ -58,6 +60,8 @@ development:
test:
<<: *default
database: budget_app_test
+ username: Dev
+ password: 12345678
# As with config/credentials.yml, you never want to store sensitive information,
# like your database password, in your source code. If your source code is
@@ -81,6 +85,7 @@ test:
#
production:
<<: *default
+ url: <%= ENV["DATABASE_URL"] %>
database: budget_app_production
username: budget_app
password: <%= ENV["BUDGET_APP_DATABASE_PASSWORD"] %>
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 8500f45..494e2ed 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -41,6 +41,8 @@
config.action_mailer.perform_caching = false
+ config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
+
# Print deprecation notices to the Rails logger.
config.active_support.deprecation = :log
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 2a95ff7..0b5771a 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -22,7 +22,7 @@
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
- config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present?
+ config.public_file_server.enabled = ENV["RAILS_SERVE_STATIC_FILES"].present? || ENV["RENDER"].present?
# Compress CSS using a preprocessor.
# config.assets.css_compressor = :sass
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
new file mode 100644
index 0000000..3ad6cfa
--- /dev/null
+++ b/config/initializers/devise.rb
@@ -0,0 +1,313 @@
+# frozen_string_literal: true
+
+# Assuming you have not yet modified this file, each configuration option below
+# is set to its default value. Note that some are commented out while others
+# are not: uncommented lines are intended to protect your configuration from
+# breaking changes in upgrades (i.e., in the event that future versions of
+# Devise change the default values for those options).
+#
+# Use this hook to configure devise mailer, warden hooks and so forth.
+# Many of these configuration options can be set straight in your model.
+Devise.setup do |config|
+ # The secret key used by Devise. Devise uses this key to generate
+ # random tokens. Changing this key will render invalid all existing
+ # confirmation, reset password and unlock tokens in the database.
+ # Devise will use the `secret_key_base` as its `secret_key`
+ # by default. You can change it below and use your own secret key.
+ # config.secret_key = '847f08822358ca24bce4421fb4cfb7c5cc598a1e785888902e0a6083549e6018052ad6417db77c7b7fbf25d05af5d6a129460f43e7abe1b3eca4c8f2908e1ec5'
+
+ # ==> Controller configuration
+ # Configure the parent class to the devise controllers.
+ # config.parent_controller = 'DeviseController'
+
+ # ==> Mailer Configuration
+ # Configure the e-mail address which will be shown in Devise::Mailer,
+ # note that it will be overwritten if you use your own mailer class
+ # with default "from" parameter.
+ config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com'
+
+ # Configure the class responsible to send e-mails.
+ # config.mailer = 'Devise::Mailer'
+
+ # Configure the parent class responsible to send e-mails.
+ # config.parent_mailer = 'ActionMailer::Base'
+
+ # ==> ORM configuration
+ # Load and configure the ORM. Supports :active_record (default) and
+ # :mongoid (bson_ext recommended) by default. Other ORMs may be
+ # available as additional gems.
+ require 'devise/orm/active_record'
+
+ # ==> Configuration for any authentication mechanism
+ # Configure which keys are used when authenticating a user. The default is
+ # just :email. You can configure it to use [:username, :subdomain], so for
+ # authenticating a user, both parameters are required. Remember that those
+ # parameters are used only when authenticating and not when retrieving from
+ # session. If you need permissions, you should implement that in a before filter.
+ # You can also supply a hash where the value is a boolean determining whether
+ # or not authentication should be aborted when the value is not present.
+ # config.authentication_keys = [:email]
+
+ # Configure parameters from the request object used for authentication. Each entry
+ # given should be a request method and it will automatically be passed to the
+ # find_for_authentication method and considered in your model lookup. For instance,
+ # if you set :request_keys to [:subdomain], :subdomain will be used on authentication.
+ # The same considerations mentioned for authentication_keys also apply to request_keys.
+ # config.request_keys = []
+
+ # Configure which authentication keys should be case-insensitive.
+ # These keys will be downcased upon creating or modifying a user and when used
+ # to authenticate or find a user. Default is :email.
+ config.case_insensitive_keys = [:email]
+
+ # Configure which authentication keys should have whitespace stripped.
+ # These keys will have whitespace before and after removed upon creating or
+ # modifying a user and when used to authenticate or find a user. Default is :email.
+ config.strip_whitespace_keys = [:email]
+
+ # Tell if authentication through request.params is enabled. True by default.
+ # It can be set to an array that will enable params authentication only for the
+ # given strategies, for example, `config.params_authenticatable = [:database]` will
+ # enable it only for database (email + password) authentication.
+ # config.params_authenticatable = true
+
+ # Tell if authentication through HTTP Auth is enabled. False by default.
+ # It can be set to an array that will enable http authentication only for the
+ # given strategies, for example, `config.http_authenticatable = [:database]` will
+ # enable it only for database authentication.
+ # For API-only applications to support authentication "out-of-the-box", you will likely want to
+ # enable this with :database unless you are using a custom strategy.
+ # The supported strategies are:
+ # :database = Support basic authentication with authentication key + password
+ # config.http_authenticatable = false
+
+ # If 401 status code should be returned for AJAX requests. True by default.
+ # config.http_authenticatable_on_xhr = true
+
+ # The realm used in Http Basic Authentication. 'Application' by default.
+ # config.http_authentication_realm = 'Application'
+
+ # It will change confirmation, password recovery and other workflows
+ # to behave the same regardless if the e-mail provided was right or wrong.
+ # Does not affect registerable.
+ # config.paranoid = true
+
+ # By default Devise will store the user in session. You can skip storage for
+ # particular strategies by setting this option.
+ # Notice that if you are skipping storage for all authentication paths, you
+ # may want to disable generating routes to Devise's sessions controller by
+ # passing skip: :sessions to `devise_for` in your config/routes.rb
+ config.skip_session_storage = [:http_auth]
+
+ # By default, Devise cleans up the CSRF token on authentication to
+ # avoid CSRF token fixation attacks. This means that, when using AJAX
+ # requests for sign in and sign up, you need to get a new CSRF token
+ # from the server. You can disable this option at your own risk.
+ # config.clean_up_csrf_token_on_authentication = true
+
+ # When false, Devise will not attempt to reload routes on eager load.
+ # This can reduce the time taken to boot the app but if your application
+ # requires the Devise mappings to be loaded during boot time the application
+ # won't boot properly.
+ # config.reload_routes = true
+
+ # ==> Configuration for :database_authenticatable
+ # For bcrypt, this is the cost for hashing the password and defaults to 12. If
+ # using other algorithms, it sets how many times you want the password to be hashed.
+ # The number of stretches used for generating the hashed password are stored
+ # with the hashed password. This allows you to change the stretches without
+ # invalidating existing passwords.
+ #
+ # Limiting the stretches to just one in testing will increase the performance of
+ # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use
+ # a value less than 10 in other environments. Note that, for bcrypt (the default
+ # algorithm), the cost increases exponentially with the number of stretches (e.g.
+ # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation).
+ config.stretches = Rails.env.test? ? 1 : 12
+
+ # Set up a pepper to generate the hashed password.
+ # config.pepper = '5608bddcddb0e1ffd2f5bfe55c8829c922f4320ebc4a64cc15862bdbd0d81f6b46eb60bdb45e3c438f6a85cadc3ea75b95b944c8aa6faeb09334fd80f22f283d'
+
+ # Send a notification to the original email when the user's email is changed.
+ # config.send_email_changed_notification = false
+
+ # Send a notification email when the user's password is changed.
+ # config.send_password_change_notification = false
+
+ # ==> Configuration for :confirmable
+ # A period that the user is allowed to access the website even without
+ # confirming their account. For instance, if set to 2.days, the user will be
+ # able to access the website for two days without confirming their account,
+ # access will be blocked just in the third day.
+ # You can also set it to nil, which will allow the user to access the website
+ # without confirming their account.
+ # Default is 0.days, meaning the user cannot access the website without
+ # confirming their account.
+ # config.allow_unconfirmed_access_for = 2.days
+
+ # A period that the user is allowed to confirm their account before their
+ # token becomes invalid. For example, if set to 3.days, the user can confirm
+ # their account within 3 days after the mail was sent, but on the fourth day
+ # their account can't be confirmed with the token any more.
+ # Default is nil, meaning there is no restriction on how long a user can take
+ # before confirming their account.
+ # config.confirm_within = 3.days
+
+ # If true, requires any email changes to be confirmed (exactly the same way as
+ # initial account confirmation) to be applied. Requires additional unconfirmed_email
+ # db field (see migrations). Until confirmed, new email is stored in
+ # unconfirmed_email column, and copied to email column on successful confirmation.
+ config.reconfirmable = true
+
+ # Defines which key will be used when confirming an account
+ # config.confirmation_keys = [:email]
+
+ # ==> Configuration for :rememberable
+ # The time the user will be remembered without asking for credentials again.
+ # config.remember_for = 2.weeks
+
+ # Invalidates all the remember me tokens when the user signs out.
+ config.expire_all_remember_me_on_sign_out = true
+
+ # If true, extends the user's remember period when remembered via cookie.
+ # config.extend_remember_period = false
+
+ # Options to be passed to the created cookie. For instance, you can set
+ # secure: true in order to force SSL only cookies.
+ # config.rememberable_options = {}
+
+ # ==> Configuration for :validatable
+ # Range for password length.
+ config.password_length = 6..128
+
+ # Email regex used to validate email formats. It simply asserts that
+ # one (and only one) @ exists in the given string. This is mainly
+ # to give user feedback and not to assert the e-mail validity.
+ config.email_regexp = /\A[^@\s]+@[^@\s]+\z/
+
+ # ==> Configuration for :timeoutable
+ # The time you want to timeout the user session without activity. After this
+ # time the user will be asked for credentials again. Default is 30 minutes.
+ # config.timeout_in = 30.minutes
+
+ # ==> Configuration for :lockable
+ # Defines which strategy will be used to lock an account.
+ # :failed_attempts = Locks an account after a number of failed attempts to sign in.
+ # :none = No lock strategy. You should handle locking by yourself.
+ # config.lock_strategy = :failed_attempts
+
+ # Defines which key will be used when locking and unlocking an account
+ # config.unlock_keys = [:email]
+
+ # Defines which strategy will be used to unlock an account.
+ # :email = Sends an unlock link to the user email
+ # :time = Re-enables login after a certain amount of time (see :unlock_in below)
+ # :both = Enables both strategies
+ # :none = No unlock strategy. You should handle unlocking by yourself.
+ # config.unlock_strategy = :both
+
+ # Number of authentication tries before locking an account if lock_strategy
+ # is failed attempts.
+ # config.maximum_attempts = 20
+
+ # Time interval to unlock the account if :time is enabled as unlock_strategy.
+ # config.unlock_in = 1.hour
+
+ # Warn on the last attempt before the account is locked.
+ # config.last_attempt_warning = true
+
+ # ==> Configuration for :recoverable
+ #
+ # Defines which key will be used when recovering the password for an account
+ # config.reset_password_keys = [:email]
+
+ # Time interval you can reset your password with a reset password key.
+ # Don't put a too small interval or your users won't have the time to
+ # change their passwords.
+ config.reset_password_within = 6.hours
+
+ # When set to false, does not sign a user in automatically after their password is
+ # reset. Defaults to true, so a user is signed in automatically after a reset.
+ # config.sign_in_after_reset_password = true
+
+ # ==> Configuration for :encryptable
+ # Allow you to use another hashing or encryption algorithm besides bcrypt (default).
+ # You can use :sha1, :sha512 or algorithms from others authentication tools as
+ # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20
+ # for default behavior) and :restful_authentication_sha1 (then you should set
+ # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper).
+ #
+ # Require the `devise-encryptable` gem when using anything other than bcrypt
+ # config.encryptor = :sha512
+
+ # ==> Scopes configuration
+ # Turn scoped views on. Before rendering "sessions/new", it will first check for
+ # "users/sessions/new". It's turned off by default because it's slower if you
+ # are using only default views.
+ # config.scoped_views = false
+
+ # Configure the default scope given to Warden. By default it's the first
+ # devise role declared in your routes (usually :user).
+ # config.default_scope = :user
+
+ # Set this configuration to false if you want /users/sign_out to sign out
+ # only the current scope. By default, Devise signs out all scopes.
+ # config.sign_out_all_scopes = true
+
+ # ==> Navigation configuration
+ # Lists the formats that should be treated as navigational. Formats like
+ # :html should redirect to the sign in page when the user does not have
+ # access, but formats like :xml or :json, should return 401.
+ #
+ # If you have any extra navigational formats, like :iphone or :mobile, you
+ # should add them to the navigational formats lists.
+ #
+ # The "*/*" below is required to match Internet Explorer requests.
+ # config.navigational_formats = ['*/*', :html, :turbo_stream]
+
+ # The default HTTP method used to sign out a resource. Default is :delete.
+ config.sign_out_via = :delete
+
+ # ==> OmniAuth
+ # Add a new OmniAuth provider. Check the wiki for more information on setting
+ # up on your models and hooks.
+ # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'
+
+ # ==> Warden configuration
+ # If you want to use other strategies, that are not supported by Devise, or
+ # change the failure app, you can configure them inside the config.warden block.
+ #
+ # config.warden do |manager|
+ # manager.intercept_401 = false
+ # manager.default_strategies(scope: :user).unshift :some_external_strategy
+ # end
+
+ # ==> Mountable engine configurations
+ # When using Devise inside an engine, let's call it `MyEngine`, and this engine
+ # is mountable, there are some extra configurations to be taken into account.
+ # The following options are available, assuming the engine is mounted as:
+ #
+ # mount MyEngine, at: '/my_engine'
+ #
+ # The router that invoked `devise_for`, in the example above, would be:
+ # config.router_name = :my_engine
+ #
+ # When using OmniAuth, Devise cannot automatically set OmniAuth path,
+ # so you need to do it manually. For the users scope, it would be:
+ # config.omniauth_path_prefix = '/my_engine/users/auth'
+
+ # ==> Hotwire/Turbo configuration
+ # When using Devise with Hotwire/Turbo, the http status for error responses
+ # and some redirects must match the following. The default in Devise for existing
+ # apps is `200 OK` and `302 Found` respectively, but new apps are generated with
+ # these new defaults that match Hotwire/Turbo behavior.
+ # Note: These might become the new default in future versions of Devise.
+ config.responder.error_status = :unprocessable_entity
+ config.responder.redirect_status = :see_other
+
+ # ==> Configuration for :registerable
+
+ # When set to false, does not sign a user in automatically after their password is
+ # changed. Defaults to true, so a user is signed in automatically after changing a password.
+ # config.sign_in_after_change_password = true
+end
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
new file mode 100644
index 0000000..260e1c4
--- /dev/null
+++ b/config/locales/devise.en.yml
@@ -0,0 +1,65 @@
+# Additional translations at https://github.com/heartcombo/devise/wiki/I18n
+
+en:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ invalid: "Invalid %{authentication_keys} or password."
+ locked: "Your account is locked."
+ last_attempt: "You have one more attempt before your account is locked."
+ not_found_in_database: "Invalid %{authentication_keys} or password."
+ timeout: "Your session expired. Please sign in again to continue."
+ unauthenticated: "You need to sign in or sign up before continuing."
+ unconfirmed: "You have to confirm your email address before continuing."
+ mailer:
+ confirmation_instructions:
+ subject: "Confirmation instructions"
+ reset_password_instructions:
+ subject: "Reset password instructions"
+ unlock_instructions:
+ subject: "Unlock instructions"
+ email_changed:
+ subject: "Email Changed"
+ password_change:
+ subject: "Password Changed"
+ omniauth_callbacks:
+ failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
+ success: "Successfully authenticated from %{kind} account."
+ passwords:
+ no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
+ send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
+ updated: "Your password has been changed successfully. You are now signed in."
+ updated_not_active: "Your password has been changed successfully."
+ registrations:
+ destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
+ signed_up: "Welcome! You have signed up successfully."
+ signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
+ signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
+ signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
+ update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirmation link to confirm your new email address."
+ updated: "Your account has been updated successfully."
+ updated_but_not_signed_in: "Your account has been updated successfully, but since your password was changed, you need to sign in again."
+ sessions:
+ signed_in: "Signed in successfully."
+ signed_out: "Signed out successfully."
+ already_signed_out: "Signed out successfully."
+ unlocks:
+ send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
+ send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
+ unlocked: "Your account has been unlocked successfully. Please sign in to continue."
+ errors:
+ messages:
+ already_confirmed: "was already confirmed, please try signing in"
+ confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
+ expired: "has expired, please request a new one"
+ not_found: "not found"
+ not_locked: "was not locked"
+ not_saved:
+ one: "1 error prohibited this %{resource} from being saved:"
+ other: "%{count} errors prohibited this %{resource} from being saved:"
diff --git a/config/puma.rb b/config/puma.rb
index daaf036..81dae67 100644
--- a/config/puma.rb
+++ b/config/puma.rb
@@ -30,14 +30,14 @@
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
-# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+workers ENV.fetch("WEB_CONCURRENCY") { 4 }
# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
-# preload_app!
+preload_app!
# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
diff --git a/config/routes.rb b/config/routes.rb
index 262ffd5..6564631 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,4 +1,20 @@
Rails.application.routes.draw do
+ devise_for :users
+
+ resources :groups, only: %i[index show new create] do
+ resources :entities, only: %i[create new]
+ end
+
+ get '/groups/:group_id/entities/new_form', to: 'entities#new', as: 'new_group_entity_form'
+
+ # make users#splash the root path only if the user is not signed in, else make groups#index the root path
+ authenticated :user do
+ root 'groups#index', as: :authenticated_root
+ end
+
+ unauthenticated do
+ root 'users#splash', as: :unauthenticated_root
+ end
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
# Defines the root path route ("/")
diff --git a/db/migrate/20231017200507_create_users.rb b/db/migrate/20231017200507_create_users.rb
new file mode 100644
index 0000000..dd02b09
--- /dev/null
+++ b/db/migrate/20231017200507_create_users.rb
@@ -0,0 +1,9 @@
+class CreateUsers < ActiveRecord::Migration[7.0]
+ def change
+ create_table :users do |t|
+ t.string :name
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20231017200833_add_devise_to_users.rb b/db/migrate/20231017200833_add_devise_to_users.rb
new file mode 100644
index 0000000..98ed9dc
--- /dev/null
+++ b/db/migrate/20231017200833_add_devise_to_users.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+class AddDeviseToUsers < ActiveRecord::Migration[7.0]
+ def self.up
+ change_table :users do |t|
+ ## Database authenticatable
+ t.string :email, null: false, default: ""
+ t.string :encrypted_password, null: false, default: ""
+
+ ## Recoverable
+ t.string :reset_password_token
+ t.datetime :reset_password_sent_at
+
+ ## Rememberable
+ t.datetime :remember_created_at
+
+ ## Trackable
+ # t.integer :sign_in_count, default: 0, null: false
+ # t.datetime :current_sign_in_at
+ # t.datetime :last_sign_in_at
+ # t.string :current_sign_in_ip
+ # t.string :last_sign_in_ip
+
+ ## Confirmable
+ # t.string :confirmation_token
+ # t.datetime :confirmed_at
+ # t.datetime :confirmation_sent_at
+ # t.string :unconfirmed_email # Only if using reconfirmable
+
+ ## Lockable
+ # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
+ # t.string :unlock_token # Only if unlock strategy is :email or :both
+ # t.datetime :locked_at
+
+
+ # Uncomment below if timestamps were not included in your original model.
+ # t.timestamps null: false
+ end
+
+ add_index :users, :email, unique: true
+ add_index :users, :reset_password_token, unique: true
+ # add_index :users, :confirmation_token, unique: true
+ # add_index :users, :unlock_token, unique: true
+ end
+
+ def self.down
+ # By default, we don't want to make any assumption about how to roll back a migration when your
+ # model already existed. Please edit below which fields you would like to remove in this migration.
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/migrate/20231017201238_create_entities.rb b/db/migrate/20231017201238_create_entities.rb
new file mode 100644
index 0000000..2b9e8fd
--- /dev/null
+++ b/db/migrate/20231017201238_create_entities.rb
@@ -0,0 +1,10 @@
+class CreateEntities < ActiveRecord::Migration[7.0]
+ def change
+ create_table :entities do |t|
+ t.string :name
+ t.float :amount
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20231017201330_create_groups.rb b/db/migrate/20231017201330_create_groups.rb
new file mode 100644
index 0000000..e3d80b8
--- /dev/null
+++ b/db/migrate/20231017201330_create_groups.rb
@@ -0,0 +1,10 @@
+class CreateGroups < ActiveRecord::Migration[7.0]
+ def change
+ create_table :groups do |t|
+ t.string :name
+ t.string :icon
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20231017202555_add_author_id_to_entity.rb b/db/migrate/20231017202555_add_author_id_to_entity.rb
new file mode 100644
index 0000000..2516425
--- /dev/null
+++ b/db/migrate/20231017202555_add_author_id_to_entity.rb
@@ -0,0 +1,6 @@
+class AddAuthorIdToEntity < ActiveRecord::Migration[7.0]
+ def change
+ # Add column author_id to table entities referencing to table users
+ add_reference :entities, :author, foreign_key: { to_table: :users }
+ end
+end
diff --git a/db/migrate/20231018062517_add_user_id_to_group.rb b/db/migrate/20231018062517_add_user_id_to_group.rb
new file mode 100644
index 0000000..1dc8445
--- /dev/null
+++ b/db/migrate/20231018062517_add_user_id_to_group.rb
@@ -0,0 +1,6 @@
+class AddUserIdToGroup < ActiveRecord::Migration[7.0]
+ def change
+ add_column :groups, :user_id, :integer
+ add_index :groups, :user_id
+ end
+end
diff --git a/db/migrate/20231018083822_create_groups_entities_join_table.rb b/db/migrate/20231018083822_create_groups_entities_join_table.rb
new file mode 100644
index 0000000..44dbbe9
--- /dev/null
+++ b/db/migrate/20231018083822_create_groups_entities_join_table.rb
@@ -0,0 +1,8 @@
+class CreateGroupsEntitiesJoinTable < ActiveRecord::Migration[7.0]
+ def change
+ create_join_table :groups, :entities do |t|
+ t.index :group_id
+ t.index :entity_id
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..5d69105
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,56 @@
+# This file is auto-generated from the current state of the database. Instead
+# of editing this file, please use the migrations feature of Active Record to
+# incrementally modify your database, and then regenerate this schema definition.
+#
+# This file is the source Rails uses to define your schema when running `bin/rails
+# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
+#
+# It's strongly recommended that you check this file into your version control system.
+
+ActiveRecord::Schema[7.0].define(version: 2023_10_18_083822) do
+ # These are extensions that must be enabled in order to support this database
+ enable_extension "plpgsql"
+
+ create_table "entities", force: :cascade do |t|
+ t.string "name"
+ t.float "amount"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.bigint "author_id"
+ t.index ["author_id"], name: "index_entities_on_author_id"
+ end
+
+ create_table "entities_groups", id: false, force: :cascade do |t|
+ t.bigint "group_id", null: false
+ t.bigint "entity_id", null: false
+ t.index ["entity_id"], name: "index_entities_groups_on_entity_id"
+ t.index ["group_id"], name: "index_entities_groups_on_group_id"
+ end
+
+ create_table "groups", force: :cascade do |t|
+ t.string "name"
+ t.string "icon"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.integer "user_id"
+ t.index ["user_id"], name: "index_groups_on_user_id"
+ end
+
+ create_table "users", force: :cascade do |t|
+ t.string "name"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
+ t.string "reset_password_token"
+ t.datetime "reset_password_sent_at"
+ t.datetime "remember_created_at"
+ t.index ["email"], name: "index_users_on_email", unique: true
+ t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
+ end
+
+ add_foreign_key "entities", "users", column: "author_id"
+end
diff --git a/render.yaml b/render.yaml
new file mode 100644
index 0000000..a91e4bb
--- /dev/null
+++ b/render.yaml
@@ -0,0 +1,18 @@
+databases:
+ - name: budget_app_production
+ databaseName: budget_app_production
+ user: budget_app
+
+services:
+ - type: web
+ name: budget-app
+ runtime: ruby
+ buildCommand: "./bin/render-build.sh"
+ startCommand: "bundle exec rails s"
+ envVars:
+ - key: DATABASE_URL
+ fromDatabase:
+ name: budget_app
+ property: connectionString
+ - key: RAILS_MASTER_KEY
+ sync: false
\ No newline at end of file
diff --git a/spec/integration/entity_spec.rb b/spec/integration/entity_spec.rb
new file mode 100644
index 0000000..e63174c
--- /dev/null
+++ b/spec/integration/entity_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.feature 'Entity Index Page' do
+ let(:user) { User.create(name: 'Jane', email: 'jane@doe.com', password: '123456') }
+ let(:group) { Group.create(name: 'Group 1', icon: 'https://cdn.iconscout.com/icon/free/png-256/ruby-47-1175102.png', user:) }
+
+ describe 'Testing integration specs for entities index page' do
+ before :each do
+ Entity.create(name: 'Entity 1', amount: 100, group_ids: [group.id])
+ login_as(user, scope: :user)
+ visit group_path(group)
+ end
+
+ context 'When visiting entities path' do
+ it 'can see entity name' do
+ expect(page).to have_content('Entity 1')
+ end
+
+ it 'can see entity amount' do
+ expect(page).to have_content('$100.0')
+ end
+
+ it 'can see link to add new entity' do
+ expect(page).to have_link('Add a new transaction', href: new_group_entity_path(group))
+ end
+ end
+ end
+end
diff --git a/spec/integration/group_spec.rb b/spec/integration/group_spec.rb
new file mode 100644
index 0000000..528daa0
--- /dev/null
+++ b/spec/integration/group_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.feature 'Group Index Page' do
+ let(:user) { User.create(name: 'Jane', email: 'jane@doe.com', password: '123456') }
+
+ describe 'Testing integration specs for groups index page' do
+ before :each do
+ Group.create(name: 'Group 1', icon: 'https://cdn.iconscout.com/icon/free/png-256/ruby-47-1175102.png',
+ user:)
+ login_as(user, scope: :user)
+ visit groups_path
+ end
+
+ context 'When visiting groups path' do
+ it 'can see group name' do
+ expect(page).to have_content('Group 1')
+ end
+
+ it 'can see group icon' do
+ expect(page).to have_css("img[src='https://cdn.iconscout.com/icon/free/png-256/ruby-47-1175102.png']")
+ end
+
+ it 'can see link to add new group' do
+ expect(page).to have_link('Add new category', href: new_group_path)
+ end
+ end
+ end
+end
diff --git a/spec/integration/user_spec.rb b/spec/integration/user_spec.rb
new file mode 100644
index 0000000..8887ea0
--- /dev/null
+++ b/spec/integration/user_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+RSpec.feature 'User Index Page' do
+ let(:user) { User.create(name: 'Jane', email: 'jane@doe.com', password: '123456') }
+
+ describe 'Testing integration specs for users index page' do
+ before :each do
+ login_as(user, scope: :user)
+ visit groups_path
+ end
+
+ context 'When visiting users path' do
+ it 'can see user login info' do
+ expect(page).to have_content('Logged in as jane@doe.com')
+ end
+
+ it 'can see logout button' do
+ expect(page).to have_button('Logout')
+ end
+ end
+ end
+end
diff --git a/spec/models/entity_spec.rb b/spec/models/entity_spec.rb
new file mode 100644
index 0000000..ac6e92f
--- /dev/null
+++ b/spec/models/entity_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+RSpec.describe Entity, type: :model do
+ before do
+ @author = User.new(name: 'John Doe')
+ @group = Group.new(name: 'Group', icon: 'icon.jpg', user: @user)
+ end
+
+ it 'is not valid without a group' do
+ entity = Entity.new(name: 'Entity', amount: 30, author_id: 1, groups: [])
+ expect(entity).to_not be_valid
+ end
+
+ it 'is not valid without a name' do
+ entity = Entity.new(amount: 30, author_id: 1, groups: [])
+ expect(entity).to_not be_valid
+ end
+
+ it 'is not valid without an amount' do
+ entity = Entity.new(name: 'Entity', author_id: 1, groups: [])
+ expect(entity).to_not be_valid
+ end
+
+ it 'is not valid without an author' do
+ entity = Entity.new(name: 'Entity', amount: 30, groups: [])
+ expect(entity).to_not be_valid
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
new file mode 100644
index 0000000..e41af9d
--- /dev/null
+++ b/spec/models/group_spec.rb
@@ -0,0 +1,27 @@
+require 'rails_helper'
+
+RSpec.describe Group, type: :model do
+ before do
+ @user = User.new(name: 'John Doe')
+ end
+
+ it 'is valid with valid attributes' do
+ group = Group.new(name: 'Group', icon: 'icon.jpg', user: @user)
+ expect(group).to be_valid
+ end
+
+ it 'is not valid without a name' do
+ group = Group.new(icon: 'icon.jpg', user_id: @user.id)
+ expect(group).to_not be_valid
+ end
+
+ it 'is not valid without an icon' do
+ group = Group.new(name: 'Group', user_id: @user.id)
+ expect(group).to_not be_valid
+ end
+
+ it 'is not valid without a user' do
+ group = Group.new(name: 'Group', icon: 'icon.jpg')
+ expect(group).to_not be_valid
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 0000000..60e4fd0
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,66 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+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.
+#
+# 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
+ abort 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"
+
+ # 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")
+
+ # Devise
+ config.include Devise::Test::IntegrationHelpers, type: :feature
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..dc50747
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,96 @@
+# 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!
+ #
+ # # This setting enables warnings. It's recommended, but in some cases may
+ # # be too noisy due to issues in dependencies.
+ # config.warnings = true
+ #
+ # # 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/application_system_test_case.rb b/test/application_system_test_case.rb
deleted file mode 100644
index d19212a..0000000
--- a/test/application_system_test_case.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require "test_helper"
-
-class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
- driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
-end
diff --git a/test/channels/application_cable/connection_test.rb b/test/channels/application_cable/connection_test.rb
deleted file mode 100644
index 800405f..0000000
--- a/test/channels/application_cable/connection_test.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-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
-end
diff --git a/test/controllers/.keep b/test/controllers/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/helpers/.keep b/test/helpers/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/integration/.keep b/test/integration/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/mailers/.keep b/test/mailers/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/models/.keep b/test/models/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/system/.keep b/test/system/.keep
deleted file mode 100644
index e69de29..0000000
diff --git a/test/test_helper.rb b/test/test_helper.rb
deleted file mode 100644
index 973526e..0000000
--- a/test/test_helper.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-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, with: :threads)
-
- # 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...
-end