+
+## Current functionality
+
+Article CRUD with comments
+User registration with email verification
+User accounts, admins can also manage all other users
+
+TODO:
+
+User roles
+- "Admin". Full service access. CRUD all articles. Edit and delete users. Create and delete all comments.
+
+- "Basic". Create, edit and delete own articles. Delete own comments. Read all articles and comments.
+
+- "Guest". View public articles and comments only.
+
+
+Tests
+- Full coverage (see Simplecov)
+
+- View spec coverage, although simplecov does not cover view specs by default
+
+- Acceptance tests (capybara?)
+
+
+Users
+- User avatars
+
+Docker
+- Dockerise the application
+
+## Getting Started
+
+The application has a seeds.rb file to create dummy users with article posts and comments. The comments will be posted on the other users articles.
+
+### Setting up the database:
+
+Create the database with
+
+```sh
+rake db:create
+```
+
+Run migrations
+
+```sh
+rake db:migrate
+```
+
+Seed the database with users with their articles and comments
+
+```sh
+rake db:seed
+```
+
+If any errors occur you can drop the database and follow the above steps again.
+
+```sh
+rake db:drop
+```
+
+### Logging in
+admin user to see full service privileges.
+```sh
+email: adminuser@email.com
+password: password
+```
+
+basic user.
+```sh
+email: basicuser@email.com
+password: password
+```
+
+guest user.
+```sh
+email: guestuser@email.com
+password: password
+```
+
+### Prerequisites
+
+### Installation
+
+1. Clone the repo
+ ```sh
+ git clone https://github.com/[your_username]/rails_blog.git
+ ```
+2. Navigate into the project directory
+ ```sh
+ cd rails_blog
+ ```
+
+
+
+
+
+## Usage
+
+### Signing up
+Sign up to create a user before you can interact with the application. After clicking 'sign up' an authentication token will be displayed within your terminal. Visit the given url to submit the token to verify your user. You MUST do this before you can sign in.
+
+### User roles
+There are 3 different types of users. "Admin", "Basic" and "Guest".
+
+Admin can see all posts whether private or public, and can also change the roles of other users. Can write posts and comments. Can edit and delete other users articles and comments.
+
+Basic users can see all posts whether private or public, but cannot modify other users. Can write articles or comments.
+
+Guests can only see public posts and comments. No other functionality.
+
+### Testing and test coverage
+- [Brakeman](https://github.com/presidentbeef/brakeman) checks for code vulnerabilities. This is run within the github workflow test.yml. To see a full breakdown of the scan locally run: ```brakeman```.
+
+- [Simplecov](https://github.com/simplecov-ruby/simplecov) runs when a test is executed ```bundle exec rspec```. To see a full breakdown of test coverage run ```open coverage/index.html``` in the root directory.
+
+### Linting and Style
+- [Rubocop](https://gist.github.com/jhass/a5ae80d87f18e53e7b56#file-rubocop-yml) for code style and linting. The ```rubocop.yaml``` config file sets the desired cops. To detect all offences enter the command ```rubocop```. To detect and change offences run ```rubocop -A```
+
+
+
+
+## Guides
+
+### Run with bare metal installation
+
+The application can be run using a bare metal installation.
+
+ ```sh
+ rails s
+ ```
+
+### Run with Docker - NOT YET IMPLEMENTED
+
+The application will be run using Docker for configuration exercises and further development.
+
+[Docker - Get started](https://docs.docker.com/get-started/)
+You can use this [guide](https://www.youtube.com/watch?v=J7hUHnQtFNo) to create the docker image for the application
+
+## License
+
+Distributed under the MIT License. See `LICENSE.txt` for more information.
+
+
+
+
+## Acknowledgments
+
+Use this space to list resources you find helpful and would like to give credit to.
+
+* [Choose an Open Source License](https://choosealicense.com)
+* [Img Shields](https://shields.io)
+* https://github.com/othneildrew/Best-README-Template
+* [Rails Getting started](https://guides.rubyonrails.org/getting_started.html)
+* [Rails Odin Project](https://www.theodinproject.com/paths/full-stack-ruby-on-rails/courses/ruby-on-rails)
+* [Rubocop config](https://gist.github.com/jhass/a5ae80d87f18e53e7b56#file-rubocop-yml)
+
+
+
+
+
+
+
+[contributors-url]: https://github.com/othneildrew/Best-README-Template/graphs/contributors
+[Bootstrap-badge]: https://img.shields.io/badge/Bootstrap-563D7C?style=for-the-badge&logo=bootstrap&logoColor=white
+[Bootstrap-url]: https://getbootstrap.com
+[Ruby-on-Rails-url]: https://rubyonrails.org
+[Rails-badge]: https://img.shields.io/badge/rails-%23CC0000.svg?style=for-the-badge&logo=ruby-on-rails&logoColor=white
[Ruby Link]: https://github.com/ministryofjustice/developer-playground/tree/ruby 'Click to view the Ruby on Rails application.'
[Ruby Icon]: https://badgen.net/badge/Ruby/on%20Rails/D30001?scale=4&labelColor=CC342D&icon=ruby
diff --git a/Rakefile b/Rakefile
new file mode 100644
index 0000000..488c551
--- /dev/null
+++ b/Rakefile
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require_relative 'config/application'
+
+Rails.application.load_tasks
diff --git a/app/.DS_Store b/app/.DS_Store
new file mode 100644
index 0000000..87bf468
Binary files /dev/null and b/app/.DS_Store differ
diff --git a/app/assets/.DS_Store b/app/assets/.DS_Store
new file mode 100644
index 0000000..9113f11
Binary files /dev/null and b/app/assets/.DS_Store differ
diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js
new file mode 100644
index 0000000..ddd546a
--- /dev/null
+++ b/app/assets/config/manifest.js
@@ -0,0 +1,4 @@
+//= link_tree ../images
+//= link_directory ../stylesheets .css
+//= link_tree ../../javascript .js
+//= link_tree ../../../vendor/javascript .js
diff --git a/app/assets/images/.keep b/app/assets/images/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/assets/stylesheets/actiontext.css b/app/assets/stylesheets/actiontext.css
new file mode 100644
index 0000000..3cfcb2b
--- /dev/null
+++ b/app/assets/stylesheets/actiontext.css
@@ -0,0 +1,31 @@
+/*
+ * Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
+ * the trix-editor content (whether displayed or under editing). Feel free to incorporate this
+ * inclusion directly in any other asset bundle and remove this file.
+ *
+ *= require trix
+*/
+
+/*
+ * We need to override trix.css’s image gallery styles to accommodate the
+ * element we wrap around attachments. Otherwise,
+ * images in galleries will be squished by the max-width: 33%; rule.
+*/
+.trix-content .attachment-gallery > action-text-attachment,
+.trix-content .attachment-gallery > .attachment {
+ flex: 1 0 33%;
+ padding: 0 0.5em;
+ max-width: 33%;
+}
+
+.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment,
+.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment,
+.trix-content .attachment-gallery.attachment-gallery--4 > .attachment {
+ flex-basis: 50%;
+ max-width: 50%;
+}
+
+.trix-content action-text-attachment .attachment {
+ padding: 0 !important;
+ max-width: 100% !important;
+}
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
new file mode 100644
index 0000000..7bdc88f
--- /dev/null
+++ b/app/assets/stylesheets/application.css
@@ -0,0 +1,115 @@
+/*
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
+ * listed below.
+ *
+ * Any CSS (and SCSS, if configured) file within this directory, lib/assets/stylesheets, or any plugin's
+ * vendor/assets/stylesheets directory can be referenced here using a relative path.
+ *
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS
+ * files in this directory. Styles in this file should be added after the last require_* statement.
+ * It is generally better to create a new file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
+
+
+h1 {
+ font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
+ font-size: 24px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 300;
+ line-height: 26px;
+}
+h2 {
+ font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
+ font-size: 20px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 300;
+ line-height: 20px;
+}
+h3 {
+ font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 300;
+ line-height: 15px;
+}
+
+p {
+ font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
+ font-size: 14px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 400;
+ line-height: 20px;
+}
+
+p.timestamp {
+ font-size: 10px;
+}
+
+blockquote {
+ font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
+ font-size: 21px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 400;
+ line-height: 30px;
+}
+
+pre {
+ font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
+ font-size: 13px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 400;
+ line-height: 18.5714px;
+}
+
+a {
+ font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif;
+ font-size: 13px;
+ font-style: normal;
+ font-variant: normal;
+ font-weight: 400;
+ line-height: 18.5714px;
+}
+
+dialog {
+ border: .5px solid grey;
+ border-radius: 5px;
+}
+
+.unread{
+ background-color: tomato;
+}
+
+.read{
+ background-color: inherit;
+}
+
+.btn-file {
+ position: relative;
+ overflow: hidden;
+}
+
+.btn-file input[type=file] {
+ position: absolute;
+ top: 0;
+ right: 0;
+ min-width: 100%;
+ min-height: 100%;
+ font-size: 100px;
+ text-align: right;
+ filter: alpha(opacity=0);
+ opacity: 0;
+ outline: none;
+ background: white;
+ cursor: inherit;
+ display: block;
+}
diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb
new file mode 100644
index 0000000..9aec230
--- /dev/null
+++ b/app/channels/application_cable/channel.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module ApplicationCable
+ class Channel < ActionCable::Channel::Base
+ end
+end
diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb
new file mode 100644
index 0000000..8d6c2a1
--- /dev/null
+++ b/app/channels/application_cable/connection.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+module ApplicationCable
+ class Connection < ActionCable::Connection::Base
+ end
+end
diff --git a/app/controllers/.DS_Store b/app/controllers/.DS_Store
new file mode 100644
index 0000000..e78f699
Binary files /dev/null and b/app/controllers/.DS_Store differ
diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb
new file mode 100644
index 0000000..8fde0e3
--- /dev/null
+++ b/app/controllers/admins_controller.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AdminsController < ApplicationController
+ before_action :authenticate_user!
+ before_action :admin_user_access
+
+ # def index
+ # @user = User.includes(:articles, :comments)
+ # end
+
+ # def show
+ # @user = User.find(params[:id])
+ # end
+
+ private
+
+ def admin_user_access
+ return if current_user&.role == 'admin'
+
+ redirect_to new_user_session_path, alert: 'Access denied: Admins only.'
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
new file mode 100644
index 0000000..f0aca2a
--- /dev/null
+++ b/app/controllers/application_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+class ApplicationController < ActionController::Base
+ include ApplicationHelper
+ # Set basic http auth before anyone can access the application
+ # http_basic_authenticate_with name: 'dhh', password: 'secret', except: %i[index show]
+
+ before_action :set_notifications, if: :current_user
+ before_action :set_query
+
+ # Allows Ransack search form to render on all views,
+ # otherwise, will only render on views/search/index.html.erb
+ def set_query
+ @q = Article.ransack(params[:q])
+ end
+
+ # All private methods ensure only the class they're initialized in can access the method
+ private
+
+ # Return all notifications for the current_user using 'where'
+ # which is an ActiveRecord::Relation object
+ # Set unread and read instance variables
+ def set_notifications
+ notifications = Notification.where(recipient: current_user).newest_first.limit(9).unread
+ @unread = notifications.unread
+ @read = notifications.read
+ end
+end
diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb
new file mode 100644
index 0000000..f2bbf66
--- /dev/null
+++ b/app/controllers/articles_controller.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+class ArticlesController < ApplicationController
+ # Ensure a user is logged in before having access
+ # unless, its the article index and show views
+ before_action :authenticate_user!
+ skip_before_action :authenticate_user!, only: %i[index show]
+
+ def index
+ @articles = Article.all
+ end
+
+ # Rails ACTIVERECORD method :include which loads associative records in
+ # advance and limits the number of SQL queries made to the DB.
+ # To improve performance.
+ def home
+ @user = User.includes(:articles, :comments)
+ end
+
+ def show
+ @article = Article.find(params[:id])
+ @comment = @article.comments.order(updated_at: :desc)
+ @article.update(views: @article.views + 1)
+
+ mark_notifications_as_read
+ end
+
+ def new
+ @article = Article.new
+ end
+
+ def edit
+ @article = Article.find(params[:id])
+ end
+
+ def create
+ @article = Article.new(article_params)
+ @article.user = current_user
+
+ if @article.save
+ redirect_to @article, notice: 'Article created successfully'
+ else
+ render :new, status: :unprocessable_entity
+ end
+ end
+
+ def update
+ @article = Article.find(params[:id])
+
+ if @article.update(article_params)
+ redirect_to @article, notice: 'Article updated successfully'
+ else
+ render :edit, status: :unprocessable_entity
+ end
+ end
+
+ def destroy
+ @article = Article.find(params[:id])
+ @article.destroy
+
+ redirect_to articles_path, status: :see_other, notice: 'Article deleted successfully'
+ end
+
+ # All private methods ensure only the class they're initialized in can access the method
+ private
+
+ def article_params
+ params.require(:article).permit(:title, :body, :status)
+ end
+
+ def mark_notifications_as_read
+ return unless current_user
+
+ notifications_to_mark_as_read = @article.notifications_as_article.where(recipient: current_user)
+ notifications_to_mark_as_read.update_all(read_at: Time.zone.now) # rubocop:disable Rails/SkipsModelValidations
+ end
+end
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
new file mode 100644
index 0000000..cba05e5
--- /dev/null
+++ b/app/controllers/comments_controller.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class CommentsController < ApplicationController
+ # Ensure a user is logged in before having access
+ # unless, its the article index and show views
+ before_action :authenticate_user!
+ before_action :set_post, on: %i[create destroy]
+
+ def edit
+ @comment = @article.comments.find(params[:id])
+ end
+
+ def create
+ @comment = @article.comments.build(comment_params)
+ @comment.user = current_user
+ @comment.save
+ redirect_to article_path(@article), notice: 'Comment added successfully'
+ end
+
+ def destroy
+ @comment = @article.comments.find(params[:id])
+ @comment.destroy
+ redirect_to article_path(@article), status: :see_other, notice: 'Comment deleted successfully'
+ end
+
+ # All private methods ensure only the class they're initialized in can access the method
+ private
+
+ # Find the article which this comment was assigned to
+ # :article_id exists in comments db schema
+ def set_post
+ @article = Article.find(params[:article_id])
+ end
+
+ # params hash ensures the given parameter (e.g :comment) is given
+ # then allows which attributes can be permitted for updating
+ def comment_params
+ params.require(:comment).permit(:body, :status)
+ end
+end
diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
new file mode 100644
index 0000000..c1c7c0f
--- /dev/null
+++ b/app/controllers/search_controller.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class SearchController < ApplicationController
+ # using ransack method, return all search matches
+ # found using search_form_for GET method
+ # sort returned results
+ def index
+ @q = Article.ransack(params[:q])
+ @articles = @q.result(distinct: true)
+ end
+end
diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb
new file mode 100644
index 0000000..663f3f9
--- /dev/null
+++ b/app/controllers/users/confirmations_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Users
+ class ConfirmationsController < Devise::ConfirmationsController
+ # GET /resource/confirmation/new
+ # def new
+ # super
+ # end
+
+ # POST /resource/confirmation
+ # def create
+ # super
+ # end
+
+ # GET /resource/confirmation?confirmation_token=abcdef
+ # def show
+ # super
+ # end
+
+ # protected
+
+ # The path used after resending confirmation instructions.
+ # def after_resending_confirmation_instructions_path_for(resource_name)
+ # super(resource_name)
+ # end
+
+ # The path used after confirmation.
+ # def after_confirmation_path_for(resource_name, resource)
+ # super(resource_name, resource)
+ # end
+ end
+end
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
new file mode 100644
index 0000000..1006dbb
--- /dev/null
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Users
+ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
+ # You should configure your model like this:
+ # devise :omniauthable, omniauth_providers: [:twitter]
+
+ # You should also create an action method in this controller like this:
+ # def twitter
+ # end
+
+ # More info at:
+ # https://github.com/heartcombo/devise#omniauth
+
+ # GET|POST /resource/auth/twitter
+ # def passthru
+ # super
+ # end
+
+ # GET|POST /users/auth/twitter/callback
+ # def failure
+ # super
+ # end
+
+ # protected
+
+ # The path used when OmniAuth fails
+ # def after_omniauth_failure_path_for(scope)
+ # super(scope)
+ # end
+ end
+end
diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb
new file mode 100644
index 0000000..23b84f9
--- /dev/null
+++ b/app/controllers/users/passwords_controller.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module Users
+ class PasswordsController < Devise::PasswordsController
+ # GET /resource/password/new
+ # def new
+ # super
+ # end
+
+ # POST /resource/password
+ # def create
+ # super
+ # end
+
+ # GET /resource/password/edit?reset_password_token=abcdef
+ # def edit
+ # super
+ # end
+
+ # PUT /resource/password
+ # def update
+ # super
+ # end
+
+ # protected
+
+ # def after_resetting_password_path_for(resource)
+ # super(resource)
+ # end
+
+ # The path used after sending reset password instructions
+ # def after_sending_reset_password_instructions_path_for(resource_name)
+ # super(resource_name)
+ # end
+ end
+end
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
new file mode 100644
index 0000000..b496071
--- /dev/null
+++ b/app/controllers/users/registrations_controller.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Users
+ class RegistrationsController < Devise::RegistrationsController
+ before_action :authenticate_user!
+ before_action :set_user, only: %i[edit update]
+ before_action :authorise_admin_or_self, only: %i[edit update]
+ before_action :configure_sign_up_params, only: [:create]
+ before_action :configure_account_update_params, only: [:update]
+
+ # PUT /users/:id - Updates the user’s information, ensuring only allowed fields are updated
+ def update
+ if @user.update(account_update_params)
+ redirect_to after_update_path_for(@user), notice: 'User updated successfully.'
+ else
+ render :edit
+ end
+ end
+
+ # Customize where users are redirected after updating their account
+ def after_update_path_for(resource)
+ current_user.admin? ? edit_user_account_path(resource) : edit_user_registration_path
+ end
+
+ private
+
+ # Retrieve the user by ID if available, otherwise use the current user
+ def set_user
+ @user = params[:id].present? ? User.find(params[:id]) : current_user
+ end
+
+ # Only allow admins or the user themselves to access the edit/update actions
+ def authorise_admin_or_self
+ return if current_user.admin? || current_user == @user
+
+ redirect_to root_path, alert: 'Access denied.'
+ end
+
+ # Permit extra parameters for sign-up
+ def configure_sign_up_params
+ devise_parameter_sanitizer.permit(:sign_up, keys: %i[username avatar])
+ end
+
+ # Permit extra parameters for account update
+ def configure_account_update_params
+ devise_parameter_sanitizer.permit(:account_update, keys: %i[username avatar role])
+ end
+ end
+end
diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb
new file mode 100644
index 0000000..311218c
--- /dev/null
+++ b/app/controllers/users/sessions_controller.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Users
+ class SessionsController < Devise::SessionsController
+ # before_action :configure_sign_in_params, only: [:create]
+
+ # GET /resource/sign_in
+ # def new
+ # super
+ # end
+
+ # POST /resource/sign_in
+ # def create
+ # super
+ # end
+
+ # DELETE /resource/sign_out
+ # def destroy
+ # super
+ # end
+
+ # protected
+
+ # If you have extra params to permit, append them to the sanitizer.
+ # def configure_sign_in_params
+ # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
+ # end
+ end
+end
diff --git a/app/controllers/users/unlocks_controller.rb b/app/controllers/users/unlocks_controller.rb
new file mode 100644
index 0000000..3301252
--- /dev/null
+++ b/app/controllers/users/unlocks_controller.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Users
+ class UnlocksController < Devise::UnlocksController
+ # GET /resource/unlock/new
+ # def new
+ # super
+ # end
+
+ # POST /resource/unlock
+ # def create
+ # super
+ # end
+
+ # GET /resource/unlock?unlock_token=abcdef
+ # def show
+ # super
+ # end
+
+ # protected
+
+ # The path used after sending unlock password instructions
+ # def after_sending_unlock_instructions_path_for(resource)
+ # super(resource)
+ # end
+
+ # The path used after unlocking the resource
+ # def after_unlock_path_for(resource)
+ # super(resource)
+ # end
+ end
+end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
new file mode 100644
index 0000000..b6c382a
--- /dev/null
+++ b/app/controllers/users_controller.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class UsersController < ApplicationController
+ def index
+ @users = User.all
+ render 'users/index'
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
new file mode 100644
index 0000000..8b1b76c
--- /dev/null
+++ b/app/helpers/application_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ApplicationHelper
+ # For comment notification indicator colour
+ def read_unread_style
+ @unread.count.zero? ? 'read' : 'unread' # rubocop:disable RSpec/HelperInstanceVariable
+ end
+
+ def present_num_of_articles
+ "The forum has #{pluralize(Article.count, 'article')}"
+ end
+
+ def present_num_of_admins
+ "The forum has #{pluralize(User.where(role: 2).count, 'admin')}"
+ end
+
+ def present_num_of_users
+ "The forum has #{pluralize(User.where(role: 1).count, 'basic user')}"
+ end
+end
diff --git a/app/helpers/articles_helper.rb b/app/helpers/articles_helper.rb
new file mode 100644
index 0000000..a090f20
--- /dev/null
+++ b/app/helpers/articles_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module ArticlesHelper
+end
diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb
new file mode 100644
index 0000000..60507ae
--- /dev/null
+++ b/app/helpers/comments_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module CommentsHelper
+end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
new file mode 100644
index 0000000..85bd729
--- /dev/null
+++ b/app/helpers/search_helper.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+module SearchHelper
+end
diff --git a/app/javascript/application.js b/app/javascript/application.js
new file mode 100644
index 0000000..815c17f
--- /dev/null
+++ b/app/javascript/application.js
@@ -0,0 +1,19 @@
+// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
+import "@hotwired/turbo-rails"
+import "controllers"
+import "trix"
+import "@rails/actiontext"
+
+
+// Modal to confirm article or comment deletion
+Turbo.setConfirmMethod(() => {
+ let dialog = document.getElementById("turbo-confirm")
+ dialog.showModal()
+
+ return new Promise((resolve, reject) => {
+ dialog.addEventListener("close", () =>{
+ resolve(dialog.returnValue == "confirm")
+ }, {once: true})
+ })
+})
+
diff --git a/app/javascript/controllers/application.js b/app/javascript/controllers/application.js
new file mode 100644
index 0000000..1213e85
--- /dev/null
+++ b/app/javascript/controllers/application.js
@@ -0,0 +1,9 @@
+import { Application } from "@hotwired/stimulus"
+
+const application = Application.start()
+
+// Configure Stimulus development experience
+application.debug = false
+window.Stimulus = application
+
+export { application }
diff --git a/app/javascript/controllers/hello_controller.js b/app/javascript/controllers/hello_controller.js
new file mode 100644
index 0000000..5975c07
--- /dev/null
+++ b/app/javascript/controllers/hello_controller.js
@@ -0,0 +1,7 @@
+import { Controller } from "@hotwired/stimulus"
+
+export default class extends Controller {
+ connect() {
+ this.element.textContent = "Hello World!"
+ }
+}
diff --git a/app/javascript/controllers/index.js b/app/javascript/controllers/index.js
new file mode 100644
index 0000000..54ad4ca
--- /dev/null
+++ b/app/javascript/controllers/index.js
@@ -0,0 +1,11 @@
+// Import and register all your controllers from the importmap under controllers/*
+
+import { application } from "controllers/application"
+
+// Eager load all controllers defined in the import map under controllers/**/*_controller
+import { eagerLoadControllersFrom } from "@hotwired/stimulus-loading"
+eagerLoadControllersFrom("controllers", application)
+
+// Lazy load controllers as they appear in the DOM (remember not to preload controllers in import map!)
+// import { lazyLoadControllersFrom } from "@hotwired/stimulus-loading"
+// lazyLoadControllersFrom("controllers", application)
diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb
new file mode 100644
index 0000000..bef3959
--- /dev/null
+++ b/app/jobs/application_job.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+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
new file mode 100644
index 0000000..a0884f7
--- /dev/null
+++ b/app/mailers/application_mailer.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class ApplicationMailer < ActionMailer::Base
+ default from: 'shaw51@hotmail.co.uk'
+ layout 'mailer'
+end
diff --git a/app/mailers/user_notifier_mailer.rb b/app/mailers/user_notifier_mailer.rb
new file mode 100644
index 0000000..a8fe264
--- /dev/null
+++ b/app/mailers/user_notifier_mailer.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class UserNotifierMailer < ApplicationMailer
+ default from: 'shaw51@hotmail.co.uk'
+
+ # Send a sign up email for the user, pass in the user object that contains the users email address
+ def send_signup_email(user)
+ @user = user
+ mail(to: @user.email,
+ subject: 'Thanks for signing up to the Car blog')
+ end
+end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
new file mode 100644
index 0000000..08dc537
--- /dev/null
+++ b/app/models/application_record.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ApplicationRecord < ActiveRecord::Base
+ primary_abstract_class
+end
diff --git a/app/models/article.rb b/app/models/article.rb
new file mode 100644
index 0000000..6dcca9e
--- /dev/null
+++ b/app/models/article.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: articles
+#
+# id :integer not null, primary key
+# title :string
+# body :text
+# created_at :datetime not null
+# updated_at :datetime not null
+# status :string
+# user_id :integer not null
+# views :integer default(0)
+#
+class Article < ApplicationRecord
+ include Visible
+ has_many :comments, dependent: :destroy
+ belongs_to :user
+
+ validates :title, presence: true, length: { minimum: 1, maximum: 40 }
+ validates :body, presence: true, length: { minimum: 10, maximum: 10_000 }
+
+ has_noticed_notifications model_name: 'Notification'
+
+ # `ransackable_attributes` by default returns all column names
+ # and any defined ransackers as an array of strings.
+ # For overriding with a whitelist array of strings.
+ def self.ransackable_attributes(_auth_object = nil)
+ column_names + _ransackers.keys
+ end
+
+ # `ransackable_associations` by default returns the names
+ # of all associations as an array of strings.
+ # For overriding with a whitelist array of strings.
+ def self.ransackable_associations(_auth_object = nil)
+ reflect_on_all_associations.map { |a| a.name.to_s }
+ end
+end
diff --git a/app/models/comment.rb b/app/models/comment.rb
new file mode 100644
index 0000000..eb7f60d
--- /dev/null
+++ b/app/models/comment.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: comments
+#
+# id :integer not null, primary key
+# commenter :string
+# body :text
+# article_id :integer not null
+# created_at :datetime not null
+# updated_at :datetime not null
+# status :string
+# user_id :integer not null
+#
+class Comment < ApplicationRecord
+ # include Visible
+
+ belongs_to :article
+ belongs_to :user
+ has_rich_text :body
+
+ after_create_commit :notify_recipient
+ before_destroy :cleanup_notifications
+ has_noticed_notifications model_name: 'Notification'
+
+ private
+
+ # Provide the parameters using .with to use them
+ # in the methods and callbacks in the instance,
+ # The deliver_later takes advantage of ACTIVE JOB, which allows
+ # emails to be sent outside of the request response cycle
+ def notify_recipient
+ CommentNotification.with(comment: self, article:).deliver_later(article.user)
+ end
+
+ def cleanup_notifications
+ notifications_as_comment.destroy_all
+ end
+end
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/concerns/visible.rb b/app/models/concerns/visible.rb
new file mode 100644
index 0000000..ff67870
--- /dev/null
+++ b/app/models/concerns/visible.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Visible
+ extend ActiveSupport::Concern
+
+ VALID_STATUSES = %w[public private archived].freeze
+
+ included do
+ validates :status, inclusion: { in: VALID_STATUSES }
+ end
+
+ class_methods do
+ def public_count
+ where(status: 'public').count
+ end
+ end
+
+ def archived?
+ status == 'archived'
+ end
+end
diff --git a/app/models/notification.rb b/app/models/notification.rb
new file mode 100644
index 0000000..90f77b3
--- /dev/null
+++ b/app/models/notification.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: notifications
+#
+# id :integer not null, primary key
+# recipient_type :string not null
+# recipient_id :integer not null
+# type :string not null
+# params :json
+# read_at :datetime
+# created_at :datetime not null
+# updated_at :datetime not null
+#
+class Notification < ApplicationRecord
+ include Noticed::Model
+ belongs_to :recipient, polymorphic: true
+end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..9ac5100
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: users
+#
+# id :integer not null, primary key
+# email :string default(""), not null
+# encrypted_password :string default(""), not null
+# reset_password_token :string
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# confirmation_token :string
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string
+# role :integer default("guest")
+# created_at :datetime not null
+# updated_at :datetime not null
+# username :string
+#
+class User < ApplicationRecord
+ # Include default devise modules. Others available are:
+ # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
+ devise :database_authenticatable, :registerable,
+ :recoverable, :rememberable, :validatable, :confirmable
+
+ has_one_attached :avatar
+ has_many :articles, dependent: :destroy
+ has_many :comments, dependent: :destroy
+ has_many :notifications, as: :recipient, dependent: :destroy
+
+ def create
+ # Create the user form params
+ @user = User.new(user_params)
+ if @user.save
+ # Send the email
+ UserNotifierMailer.send_signup_email(@user).deliver
+ redirect_to(@user, notice: 'User created')
+ else
+ render action: 'new'
+ end
+ end
+
+ def resize_image
+ # Guard clause
+ return unless avatar.content_type.in?(%w[image/jpeg image/png image/jpg])
+
+ avatar.variant(resize_to_limit: [200, 200])
+ end
+
+ private
+
+ def user_params
+ params.require(:user).permit(:name, :email, :login)
+ end
+
+ # set user privileges within console using:
+ # @user = User.first -> @user.role = 1 # @user.save
+ # guest = non-signed in user
+ # basic = signed in user
+ # admin = superuser with extra features (view/edit/delete user details, edit all articles and comments)
+ enum :role, { guest: 0, basic: 1, admin: 2 }
+ after_initialize :set_default_role, if: :new_record?
+
+ def set_default_role
+ self.role ||= :guest
+ end
+
+ def self.ransackable_attributes(_auth_object = nil) # rubocop:disable Lint/IneffectiveAccessModifier
+ ['username']
+ end
+end
diff --git a/app/notifications/comment_notification.rb b/app/notifications/comment_notification.rb
new file mode 100644
index 0000000..f4950bf
--- /dev/null
+++ b/app/notifications/comment_notification.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# To deliver this notification:
+#
+# CommentNotification.with(post: @post).deliver_later(current_user)
+# CommentNotification.with(post: @post).deliver(current_user)
+
+class CommentNotification < Noticed::Base
+ # Add your delivery methods
+ #
+ deliver_by :database
+ # deliver_by :email, mailer: "UserMailer"
+ # deliver_by :slack
+ # deliver_by :custom, class: "MyDeliveryMethod"
+
+ # Add required params
+ #
+ # param :post
+
+ # Define helper methods to make rendering easier.
+
+ def message
+ @article = Article.find(params[:comment][:article_id])
+ @comment = Comment.find(params[:comment][:id])
+ @user = User.find(@comment.user_id)
+ "#{@user.username} commented on #{@article.title.truncate(15)}"
+ end
+
+ def url
+ article_path(Article.find(params[:comment][:article_id]))
+ end
+end
diff --git a/app/views/.DS_Store b/app/views/.DS_Store
new file mode 100644
index 0000000..1b5cdfc
Binary files /dev/null and b/app/views/.DS_Store differ
diff --git a/app/views/active_storage/blobs/_blob.html.erb b/app/views/active_storage/blobs/_blob.html.erb
new file mode 100644
index 0000000..49ba357
--- /dev/null
+++ b/app/views/active_storage/blobs/_blob.html.erb
@@ -0,0 +1,14 @@
+
diff --git a/app/views/admins/_index.html.erb b/app/views/admins/_index.html.erb
new file mode 100644
index 0000000..7f9e2ae
--- /dev/null
+++ b/app/views/admins/_index.html.erb
@@ -0,0 +1,9 @@
+<%= present_num_of_admins %>
+
+
+ <% User.all.where(role: 2).each do |user| %>
+
+ <%= link_to edit_user_account_path(user) %>
+
+ <% end %>
+
\ No newline at end of file
diff --git a/app/views/admins/home.html.erb b/app/views/admins/home.html.erb
new file mode 100644
index 0000000..ce34f75
--- /dev/null
+++ b/app/views/admins/home.html.erb
@@ -0,0 +1,6 @@
+
You have written <%= pluralize(current_user.comments.count, "comments")%>
+
+ <% current_user.comments.each do |comment| %>
+
+ <%= comment.body %>
+
+ <% end %>
+
+
+
+
\ No newline at end of file
diff --git a/app/views/articles/index.html.erb b/app/views/articles/index.html.erb
new file mode 100644
index 0000000..5f074a4
--- /dev/null
+++ b/app/views/articles/index.html.erb
@@ -0,0 +1,20 @@
+
\ No newline at end of file
diff --git a/app/views/articles/new.html.erb b/app/views/articles/new.html.erb
new file mode 100644
index 0000000..024cdbe
--- /dev/null
+++ b/app/views/articles/new.html.erb
@@ -0,0 +1,2 @@
+
New Article
+<%= render "form", article: @article %>
\ No newline at end of file
diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb
new file mode 100644
index 0000000..3dbea62
--- /dev/null
+++ b/app/views/articles/show.html.erb
@@ -0,0 +1,33 @@
+
<%= @article.title %>
+
+
+
+
<%= @article.user.username %> on: <%= @article.created_at.strftime("%d %b %y %H:%M") %>
+
+
+<%# end %>
\ No newline at end of file
diff --git a/app/views/comments/_form.html.erb b/app/views/comments/_form.html.erb
new file mode 100644
index 0000000..2768ba4
--- /dev/null
+++ b/app/views/comments/_form.html.erb
@@ -0,0 +1,16 @@
+<%= form_with model: [@article, @article.comments.build] do |form| %>
+
+<% end %>
+
+<%= render "devise/shared/links" %>
diff --git a/app/views/layouts/_flash.html.erb b/app/views/layouts/_flash.html.erb
new file mode 100644
index 0000000..3721485
--- /dev/null
+++ b/app/views/layouts/_flash.html.erb
@@ -0,0 +1,11 @@
+<% if flash[:notice] %>
+
+ <%= flash[:notice] %>
+
+<% end %>
+
+<% if flash[:warning] %>
+
+ <%= flash[:warning] %>
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/layouts/_modal.html.erb b/app/views/layouts/_modal.html.erb
new file mode 100644
index 0000000..2922acc
--- /dev/null
+++ b/app/views/layouts/_modal.html.erb
@@ -0,0 +1,15 @@
+
\ No newline at end of file
diff --git a/app/views/layouts/_nav.html.erb b/app/views/layouts/_nav.html.erb
new file mode 100644
index 0000000..9706133
--- /dev/null
+++ b/app/views/layouts/_nav.html.erb
@@ -0,0 +1,57 @@
+
+
diff --git a/app/views/layouts/_notifications.html.erb b/app/views/layouts/_notifications.html.erb
new file mode 100644
index 0000000..7210541
--- /dev/null
+++ b/app/views/layouts/_notifications.html.erb
@@ -0,0 +1,18 @@
+
+<% end %>
\ No newline at end of file
diff --git a/app/views/search/index.html.erb b/app/views/search/index.html.erb
new file mode 100644
index 0000000..3cc25f3
--- /dev/null
+++ b/app/views/search/index.html.erb
@@ -0,0 +1,12 @@
+
\ No newline at end of file
diff --git a/bin/bundle b/bin/bundle
new file mode 100755
index 0000000..8012441
--- /dev/null
+++ b/bin/bundle
@@ -0,0 +1,113 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+#
+# This file was generated by Bundler.
+#
+# The application 'bundle' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'rubygems'
+
+m = Module.new do
+ module_function
+
+ def invoked_as_script?
+ File.expand_path($PROGRAM_NAME) == File.expand_path(__FILE__)
+ end
+
+ def env_var_version
+ ENV['BUNDLER_VERSION']
+ end
+
+ def cli_arg_version
+ return unless invoked_as_script? # don't want to hijack other binstubs
+ return unless 'update'.start_with?(ARGV.first || ' ') # must be running `bundle update`
+
+ bundler_version = nil
+ update_index = nil
+ ARGV.each_with_index do |a, i|
+ bundler_version = a if update_index && update_index.succ == i && a =~ Gem::Version::ANCHORED_VERSION_PATTERN
+ next unless a =~ /\A--bundler(?:[= ](#{Gem::Version::VERSION_PATTERN}))?\z/
+
+ bundler_version = Regexp.last_match(1)
+ update_index = i
+ end
+ bundler_version
+ end
+
+ def gemfile
+ gemfile = ENV['BUNDLE_GEMFILE']
+ return gemfile if gemfile && !gemfile.empty?
+
+ File.expand_path('../Gemfile', __dir__)
+ end
+
+ def lockfile
+ lockfile =
+ case File.basename(gemfile)
+ when 'gems.rb' then gemfile.sub(/\.rb$/, '.locked')
+ else "#{gemfile}.lock"
+ end
+ File.expand_path(lockfile)
+ end
+
+ def lockfile_version
+ return unless File.file?(lockfile)
+
+ lockfile_contents = File.read(lockfile)
+ return unless lockfile_contents =~ /\n\nBUNDLED WITH\n\s{2,}(#{Gem::Version::VERSION_PATTERN})\n/
+
+ Regexp.last_match(1)
+ end
+
+ def bundler_requirement
+ @bundler_requirement ||=
+ env_var_version ||
+ cli_arg_version ||
+ bundler_requirement_for(lockfile_version)
+ end
+
+ def bundler_requirement_for(version)
+ return "#{Gem::Requirement.default}.a" unless version
+
+ bundler_gem_version = Gem::Version.new(version)
+
+ bundler_gem_version.approximate_recommendation
+ end
+
+ def load_bundler!
+ ENV['BUNDLE_GEMFILE'] ||= gemfile
+
+ activate_bundler
+ end
+
+ def activate_bundler
+ gem_error = activation_error_handling do
+ gem 'bundler', bundler_requirement
+ end
+ return if gem_error.nil?
+
+ require_error = activation_error_handling do
+ require 'bundler/version'
+ end
+ if require_error.nil? && Gem::Requirement.new(bundler_requirement).satisfied_by?(Gem::Version.new(Bundler::VERSION))
+ return
+ end
+
+ warn "Activating bundler (#{bundler_requirement}) failed:\n#{gem_error.message}\n\nTo install the version of bundler this project requires, run `gem install bundler -v '#{bundler_requirement}'`"
+ exit 42
+ end
+
+ def activation_error_handling
+ yield
+ nil
+ rescue StandardError, LoadError => e
+ e
+ end
+end
+
+m.load_bundler!
+
+load Gem.bin_path('bundler', 'bundle') if m.invoked_as_script?
diff --git a/bin/importmap b/bin/importmap
new file mode 100755
index 0000000..d423864
--- /dev/null
+++ b/bin/importmap
@@ -0,0 +1,5 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../config/application'
+require 'importmap/commands'
diff --git a/bin/rails b/bin/rails
new file mode 100755
index 0000000..a31728a
--- /dev/null
+++ b/bin/rails
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+APP_PATH = File.expand_path('../config/application', __dir__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/bin/rake b/bin/rake
new file mode 100755
index 0000000..c199955
--- /dev/null
+++ b/bin/rake
@@ -0,0 +1,6 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 0000000..516b651
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,35 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require 'fileutils'
+
+# path to your application root.
+APP_ROOT = File.expand_path('..', __dir__)
+
+def system!(*args)
+ system(*args) || abort("\n== Command #{args} failed ==")
+end
+
+FileUtils.chdir APP_ROOT do
+ # This script is a way to set up or update your development environment automatically.
+ # This script is idempotent, so that you can run it at any time and get an expectable outcome.
+ # Add necessary setup steps to this file.
+
+ puts '== Installing dependencies =='
+ system! 'gem install bundler --conservative'
+ system('bundle check') || system!('bundle install')
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # FileUtils.cp "config/database.yml.sample", "config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system! 'bin/rails db:prepare'
+
+ puts "\n== Removing old logs and tempfiles =="
+ system! 'bin/rails log:clear tmp:clear'
+
+ puts "\n== Restarting application server =="
+ system! 'bin/rails restart'
+end
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000..6dc8321
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# This file is used by Rack-based servers to start the application.
+
+require_relative 'config/environment'
+
+run Rails.application
+Rails.application.load_server
diff --git a/config/.DS_Store b/config/.DS_Store
new file mode 100644
index 0000000..c66e3dc
Binary files /dev/null and b/config/.DS_Store differ
diff --git a/config/application.rb b/config/application.rb
new file mode 100644
index 0000000..98ec4cb
--- /dev/null
+++ b/config/application.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require_relative 'boot'
+
+require 'rails/all'
+
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(*Rails.groups)
+
+module Project
+ class Application < Rails::Application
+ # Initialize configuration defaults for originally generated Rails version.
+ config.load_defaults 7.0
+
+ # Configuration for the application, engines, and railties goes here.
+ #
+ # These settings can be overridden in specific environments using the files
+ # in config/environments, which are processed later.
+ #
+ # config.time_zone = "Central Time (US & Canada)"
+ # config.eager_load_paths << Rails.root.join("extras")
+ end
+end
diff --git a/config/boot.rb b/config/boot.rb
new file mode 100644
index 0000000..c04863f
--- /dev/null
+++ b/config/boot.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
+
+require 'bundler/setup' # Set up gems listed in the Gemfile.
+require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
diff --git a/config/cable.yml b/config/cable.yml
new file mode 100644
index 0000000..9ba754d
--- /dev/null
+++ b/config/cable.yml
@@ -0,0 +1,10 @@
+development:
+ adapter: async
+
+test:
+ adapter: test
+
+production:
+ adapter: redis
+ url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
+ channel_prefix: project_production
diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc
new file mode 100644
index 0000000..1f3d6c2
--- /dev/null
+++ b/config/credentials.yml.enc
@@ -0,0 +1 @@
+vmeK21pd0FMk1oiceSU25IdxEdIv3T2MpOSmv3qcZ14dn2cVpA6oYJV0qTc6Bcmp9pRlYLKlMB1kWtrSjaC1NJllzE+gI0oFDw/tSeYjvx2Tyviv31wdeHwSjwfzicDk1B/LbJ/YonVUUHNv5Ibc63YcYhgGxWYWdtYNYwon69yoXsyF5PbQF3exipt+Ju0gkCb8qevgOgg6u/8aAt4Xxao3ge9+WB2RBvcb/Fd2zbdNhQIYqd5/DJK+EZsBMt73yR8Sh/CHy/bZ7MP19UT2O+k9E+GIExfdLgVgB77xEm2gn1rqJ9oVXjzYGiY61rFEvmRD+FFBKTQ8lVyWPjc1t+6ZFNwKeLkiq72syugU9II4ysGs/FQhJDVkFS6ZOUSDu4miAY3gDkAhzOk8iXQ9vMSbzzoT--zGbgwhVWQ6Mh4SiT--fRX7tQXBEDNXuJtcC4ID2w==
\ No newline at end of file
diff --git a/config/database.yml b/config/database.yml
new file mode 100644
index 0000000..fcba57f
--- /dev/null
+++ b/config/database.yml
@@ -0,0 +1,25 @@
+# SQLite. Versions 3.8.0 and up are supported.
+# gem install sqlite3
+#
+# Ensure the SQLite 3 gem is defined in your Gemfile
+# gem "sqlite3"
+#
+default: &default
+ adapter: sqlite3
+ pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+ timeout: 5000
+
+development:
+ <<: *default
+ database: db/development.sqlite3
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ <<: *default
+ database: db/test.sqlite3
+
+production:
+ <<: *default
+ database: db/production.sqlite3
diff --git a/config/environment.rb b/config/environment.rb
new file mode 100644
index 0000000..35e1666
--- /dev/null
+++ b/config/environment.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# Load the Rails application.
+require_relative 'application'
+
+# Initialize the Rails application.
+Rails.application.initialize!
+
+ActionMailer::Base.delivery_method = :smtp
+ActionMailer::Base.perform_deliveries = true
+ActionMailer::Base.smtp_settings = {
+ user_name: 'apikey',
+ password: Rails.application.credentials.dig(:sendgrid, :api_key),
+ domain: 'hotmail.com',
+ address: 'smtp.sendgrid.net',
+ port: 587,
+ authentication: :plain,
+ enable_starttls_auto: true
+}
diff --git a/config/environments/development.rb b/config/environments/development.rb
new file mode 100644
index 0000000..666e48c
--- /dev/null
+++ b/config/environments/development.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # In the development environment your application's code is reloaded any time
+ # it changes. This slows down response time but is perfect for development
+ # since you don't have to restart the web server when you make code changes.
+ config.cache_classes = false
+
+ # Do not eager load code on boot.
+ config.eager_load = false
+
+ # Show full error reports.
+ config.consider_all_requests_local = true
+
+ # Enable server timing
+ config.server_timing = true
+
+ # Enable/disable caching. By default caching is disabled.
+ # Run rails dev:cache to toggle caching.
+ if Rails.root.join('tmp/caching-dev.txt').exist?
+ config.action_controller.perform_caching = true
+ config.action_controller.enable_fragment_cache_logging = true
+
+ config.cache_store = :memory_store
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{2.days.to_i}"
+ }
+ else
+ config.action_controller.perform_caching = false
+
+ config.cache_store = :null_store
+ end
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Don't care if the mailer can't send.
+ config.action_mailer.raise_delivery_errors = false
+ config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
+
+ config.action_mailer.perform_caching = false
+
+ # Print deprecation notices to the Rails logger.
+ config.active_support.deprecation = :log
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
+
+ # Highlight code that triggered database queries in logs.
+ config.active_record.verbose_query_logs = true
+
+ # Suppress logger output for asset requests.
+ config.assets.quiet = true
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+
+ # Uncomment if you wish to allow Action Cable access from any origin.
+ # config.action_cable.disable_request_forgery_protection = true
+end
diff --git a/config/environments/production.rb b/config/environments/production.rb
new file mode 100644
index 0000000..ff0f6eb
--- /dev/null
+++ b/config/environments/production.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Code is not reloaded between requests.
+ config.cache_classes = true
+
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both threaded web servers
+ # and those relying on copy on write to perform better.
+ # Rake tasks automatically ignore this option for performance.
+ config.eager_load = true
+
+ # Full error reports are disabled and caching is turned on.
+ config.consider_all_requests_local = false
+ config.action_controller.perform_caching = true
+
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"]
+ # or in config/master.key. This key is used to decrypt credentials (and other encrypted files).
+ # config.require_master_key = true
+
+ # 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?
+
+ # Compress CSS using a preprocessor.
+ # config.assets.css_compressor = :sass
+
+ # Do not fallback to assets pipeline if a precompiled asset is missed.
+ config.assets.compile = false
+
+ # Enable serving of images, stylesheets, and JavaScripts from an asset server.
+ # config.asset_host = "http://assets.example.com"
+
+ # Specifies the header that your server uses for sending files.
+ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
+ # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX
+
+ # Store uploaded files on the local file system (see config/storage.yml for options).
+ config.active_storage.service = :local
+
+ # Mount Action Cable outside main process or domain.
+ # config.action_cable.mount_path = nil
+ # config.action_cable.url = "wss://example.com/cable"
+ # config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/ ]
+
+ # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
+ # config.force_ssl = true
+
+ # Include generic and useful information about system operation, but avoid logging too much
+ # information to avoid inadvertent exposure of personally identifiable information (PII).
+ config.log_level = :info
+
+ # Prepend all log lines with the following tags.
+ config.log_tags = [:request_id]
+
+ # Use a different cache store in production.
+ # config.cache_store = :mem_cache_store
+
+ # Use a real queuing backend for Active Job (and separate queues per environment).
+ # config.active_job.queue_adapter = :resque
+ # config.active_job.queue_name_prefix = "project_production"
+
+ config.action_mailer.perform_caching = false
+
+ # Ignore bad email addresses and do not raise email delivery errors.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
+ # config.action_mailer.raise_delivery_errors = false
+
+ # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
+ # the I18n.default_locale when a translation cannot be found).
+ config.i18n.fallbacks = true
+
+ # Don't log any deprecations.
+ config.active_support.report_deprecations = false
+
+ # Use default logging formatter so that PID and timestamp are not suppressed.
+ config.log_formatter = Logger::Formatter.new
+
+ # Use a different logger for distributed setups.
+ # require "syslog/logger"
+ # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new "app-name")
+
+ if ENV['RAILS_LOG_TO_STDOUT'].present?
+ logger = ActiveSupport::Logger.new($stdout)
+ logger.formatter = config.log_formatter
+ config.logger = ActiveSupport::TaggedLogging.new(logger)
+ end
+
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+end
diff --git a/config/environments/test.rb b/config/environments/test.rb
new file mode 100644
index 0000000..8f3f63c
--- /dev/null
+++ b/config/environments/test.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'active_support/core_ext/integer/time'
+
+# The test environment is used exclusively to run your application's
+# test suite. You never need to work with it otherwise. Remember that
+# your test database is "scratch space" for the test suite and is wiped
+# and recreated between test runs. Don't rely on the data there!
+
+Rails.application.configure do
+ # Settings specified here will take precedence over those in config/application.rb.
+
+ # Turn false under Spring and add config.action_view.cache_template_loading = true.
+ config.cache_classes = true
+
+ # Eager loading loads your whole application. When running a single test locally,
+ # this probably isn't necessary. It's a good idea to do in a continuous integration
+ # system, or in some way before deploying your code.
+ config.eager_load = ENV['CI'].present?
+
+ # Configure public file server for tests with Cache-Control for performance.
+ config.public_file_server.enabled = true
+ config.public_file_server.headers = {
+ 'Cache-Control' => "public, max-age=#{1.hour.to_i}"
+ }
+
+ # Show full error reports and disable caching.
+ config.consider_all_requests_local = true
+ config.action_controller.perform_caching = false
+ config.cache_store = :null_store
+
+ # Raise exceptions instead of rendering exception templates.
+ config.action_dispatch.show_exceptions = false
+
+ # Disable request forgery protection in test environment.
+ config.action_controller.allow_forgery_protection = false
+
+ # Store uploaded files on the local file system in a temporary directory.
+ config.active_storage.service = :test
+
+ config.action_mailer.perform_caching = false
+
+ # Tell Action Mailer not to deliver emails to the real world.
+ # The :test delivery method accumulates sent emails in the
+ # ActionMailer::Base.deliveries array.
+ config.action_mailer.delivery_method = :test
+
+ # Print deprecation notices to the stderr.
+ config.active_support.deprecation = :stderr
+
+ # Raise exceptions for disallowed deprecations.
+ config.active_support.disallowed_deprecation = :raise
+
+ # Tell Active Support which deprecation messages to disallow.
+ config.active_support.disallowed_deprecation_warnings = []
+
+ # Raises error for missing translations.
+ # config.i18n.raise_on_missing_translations = true
+
+ # Annotate rendered view with file names.
+ # config.action_view.annotate_rendered_view_with_filenames = true
+end
diff --git a/config/importmap.rb b/config/importmap.rb
new file mode 100644
index 0000000..f92b2e7
--- /dev/null
+++ b/config/importmap.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+# Pin npm packages by running ./bin/importmap
+
+pin 'application', preload: true
+pin '@hotwired/turbo-rails', to: 'turbo.min.js', preload: true
+pin '@hotwired/stimulus', to: 'stimulus.min.js', preload: true
+pin '@hotwired/stimulus-loading', to: 'stimulus-loading.js', preload: true
+pin_all_from 'app/javascript/controllers', under: 'controllers'
+pin 'trix'
+pin '@rails/actiontext', to: 'actiontext.js'
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb
new file mode 100644
index 0000000..bcafccd
--- /dev/null
+++ b/config/initializers/assets.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path.
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in the app/assets
+# folder are already added.
+# Rails.application.config.assets.precompile += %w( admin.js admin.css )
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
new file mode 100644
index 0000000..53538c1
--- /dev/null
+++ b/config/initializers/content_security_policy.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Define an application-wide content security policy.
+# See the Securing Rails Applications Guide for more information:
+# https://guides.rubyonrails.org/security.html#content-security-policy-header
+
+# Rails.application.configure do
+# config.content_security_policy do |policy|
+# policy.default_src :self, :https
+# policy.font_src :self, :https, :data
+# policy.img_src :self, :https, :data
+# policy.object_src :none
+# policy.script_src :self, :https
+# policy.style_src :self, :https
+# # Specify URI for violation reports
+# # policy.report_uri "/csp-violation-report-endpoint"
+# end
+#
+# # Generate session nonces for permitted importmap and inline scripts
+# config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
+# config.content_security_policy_nonce_directives = %w(script-src)
+#
+# # Report violations without enforcing the policy.
+# # config.content_security_policy_report_only = true
+# end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
new file mode 100644
index 0000000..1fceca5
--- /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 = '6d78931a808ee7b49dba7146528bb36a4e6b21eb806991ccb0a771dd1980c227b1443610ad64d62320d32afc9018b6e0502ea4a8a8f7d185d7117f1cf31b77e7'
+
+ # ==> 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 = 'shaw51@hotmail.co.uk'
+
+ # 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 = '1e2b907f7addc9eaa48645b09821b8e00993d266bba14be9f20686aa6f19e5657b713fd20d5145a1c1f8d3fd401c62fb5b0a9bb5dfba0a50faf0beb83369ee5e'
+
+ # 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/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000..3df77c5
--- /dev/null
+++ b/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Configure parameters to be filtered from the log file. Use this to limit dissemination of
+# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
+# notations and behaviors.
+Rails.application.config.filter_parameters += %i[
+ passw secret token _key crypt salt certificate otp ssn
+]
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
new file mode 100644
index 0000000..9e049dc
--- /dev/null
+++ b/config/initializers/inflections.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# Be sure to restart your server when you modify this file.
+
+# Add new inflection rules using the following format. Inflections
+# are locale specific, and you may define rules for as many different
+# locales as you wish. All of these examples are active by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.plural /^(ox)$/i, "\\1en"
+# inflect.singular /^(ox)en/i, "\\1"
+# inflect.irregular "person", "people"
+# inflect.uncountable %w( fish sheep )
+# end
+
+# These inflection rules are supported but not enabled by default:
+# ActiveSupport::Inflector.inflections(:en) do |inflect|
+# inflect.acronym "RESTful"
+# end
diff --git a/config/initializers/permissions_policy.rb b/config/initializers/permissions_policy.rb
new file mode 100644
index 0000000..810aade
--- /dev/null
+++ b/config/initializers/permissions_policy.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+# Define an application-wide HTTP permissions policy. For further
+# information see https://developers.google.com/web/updates/2018/06/feature-policy
+#
+# Rails.application.config.permissions_policy do |f|
+# f.camera :none
+# f.gyroscope :none
+# f.microphone :none
+# f.usb :none
+# f.fullscreen :self
+# f.payment :self, "https://secure.example.com"
+# 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/locales/en.yml b/config/locales/en.yml
new file mode 100644
index 0000000..8ca56fc
--- /dev/null
+++ b/config/locales/en.yml
@@ -0,0 +1,33 @@
+# Files in the config/locales directory are used for internationalization
+# and are automatically loaded by Rails. If you want to use locales other
+# than English, add the necessary files in this directory.
+#
+# To use the locales, use `I18n.t`:
+#
+# I18n.t "hello"
+#
+# In views, this is aliased to just `t`:
+#
+# <%= t("hello") %>
+#
+# To use a different locale, set it with `I18n.locale`:
+#
+# I18n.locale = :es
+#
+# This would use the information in config/locales/es.yml.
+#
+# The following keys must be escaped otherwise they will not be retrieved by
+# the default I18n backend:
+#
+# true, false, on, off, yes, no
+#
+# Instead, surround them with single quotes.
+#
+# en:
+# "true": "foo"
+#
+# To learn more, please read the Rails Internationalization guide
+# available at https://guides.rubyonrails.org/i18n.html.
+
+en:
+ hello: "Hello world"
diff --git a/config/puma.rb b/config/puma.rb
new file mode 100644
index 0000000..1713441
--- /dev/null
+++ b/config/puma.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+# Puma can serve each request in a thread from an internal thread pool.
+# The `threads` method setting takes two numbers: a minimum and maximum.
+# Any libraries that use thread pools should be configured to match
+# the maximum value specified for Puma. Default is set to 5 threads for minimum
+# and maximum; this matches the default thread size of Active Record.
+#
+max_threads_count = ENV.fetch('RAILS_MAX_THREADS', 5)
+min_threads_count = ENV.fetch('RAILS_MIN_THREADS') { max_threads_count }
+threads min_threads_count, max_threads_count
+
+# Specifies the `worker_timeout` threshold that Puma will use to wait before
+# terminating a worker in development environments.
+#
+worker_timeout 3600 if ENV.fetch('RAILS_ENV', 'development') == 'development'
+
+# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
+#
+port ENV.fetch('PORT', 3000)
+
+# Specifies the `environment` that Puma will run in.
+#
+environment ENV.fetch('RAILS_ENV', 'development')
+
+# Specifies the `pidfile` that Puma will use.
+pidfile ENV.fetch('PIDFILE', 'tmp/pids/server.pid')
+
+# Specifies the number of `workers` to boot in clustered mode.
+# Workers are forked web server processes. If using threads and workers together
+# the concurrency of the application would be max `threads` * `workers`.
+# Workers do not work on JRuby or Windows (both of which do not support
+# processes).
+#
+# workers ENV.fetch("WEB_CONCURRENCY") { 2 }
+
+# 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!
+
+# Allow puma to be restarted by `bin/rails restart` command.
+plugin :tmp_restart
diff --git a/config/routes.rb b/config/routes.rb
new file mode 100644
index 0000000..4dfb82a
--- /dev/null
+++ b/config/routes.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+# == Route Map
+#
+
+Rails.application.routes.draw do
+ root 'articles#home'
+ get 'search', to: 'search#index'
+
+ resources :admins do
+ collection do
+ get :home
+ end
+ end
+
+ devise_for :users, controllers: {
+ sessions: 'users/sessions',
+ registrations: 'users/registrations'
+ }
+
+ # Route to edit any user’s account by their ID
+ devise_scope :user do
+ get '/users/:id/edit', to: 'users/registrations#edit', as: 'edit_user_account'
+ put '/users/:id', to: 'users/registrations#update'
+ end
+
+ # allows for chained url routes
+ # eg. /posts/1/comments/4
+ resources :articles do
+ resources :comments
+ end
+end
diff --git a/config/storage.yml b/config/storage.yml
new file mode 100644
index 0000000..4eaf1fa
--- /dev/null
+++ b/config/storage.yml
@@ -0,0 +1,34 @@
+test:
+ service: Disk
+ root: <%= Rails.root.join("tmp/storage") %>
+
+local:
+ service: Disk
+ root: <%= Rails.root.join("storage") %>
+
+# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
+# amazon:
+# service: S3
+# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
+# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
+# region: us-east-1
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Remember not to checkin your GCS keyfile to a repository
+# google:
+# service: GCS
+# rails_project: your_project
+# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %>
+# bucket: your_own_bucket-<%= Rails.env %>
+
+# Use bin/rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key)
+# microsoft:
+# service: AzureStorage
+# storage_account_name: your_account_name
+# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %>
+# container: your_container_name-<%= Rails.env %>
+
+# mirror:
+# service: Mirror
+# primary: local
+# mirrors: [ amazon, google, microsoft ]
diff --git a/db/migrate/20230426175922_create_articles.rb b/db/migrate/20230426175922_create_articles.rb
new file mode 100644
index 0000000..0099ff0
--- /dev/null
+++ b/db/migrate/20230426175922_create_articles.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class CreateArticles < ActiveRecord::Migration[7.0]
+ def change
+ create_table :articles do |t|
+ t.string :title
+ t.text :body
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20230503081057_create_comments.rb b/db/migrate/20230503081057_create_comments.rb
new file mode 100644
index 0000000..241b275
--- /dev/null
+++ b/db/migrate/20230503081057_create_comments.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class CreateComments < ActiveRecord::Migration[7.0]
+ def change
+ create_table :comments do |t|
+ t.string :commenter
+ t.text :body
+ t.references :article, null: false, foreign_key: true
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20230504081718_add_status_to_articles.rb b/db/migrate/20230504081718_add_status_to_articles.rb
new file mode 100644
index 0000000..018fae7
--- /dev/null
+++ b/db/migrate/20230504081718_add_status_to_articles.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddStatusToArticles < ActiveRecord::Migration[7.0]
+ def change
+ add_column :articles, :status, :string
+ end
+end
diff --git a/db/migrate/20230504081732_add_status_to_comments.rb b/db/migrate/20230504081732_add_status_to_comments.rb
new file mode 100644
index 0000000..f23ae99
--- /dev/null
+++ b/db/migrate/20230504081732_add_status_to_comments.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddStatusToComments < ActiveRecord::Migration[7.0]
+ def change
+ add_column :comments, :status, :string
+ end
+end
diff --git a/db/migrate/20230509130515_devise_create_users.rb b/db/migrate/20230509130515_devise_create_users.rb
new file mode 100644
index 0000000..8b466df
--- /dev/null
+++ b/db/migrate/20230509130515_devise_create_users.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+class DeviseCreateUsers < ActiveRecord::Migration[7.0]
+ def change
+ create_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
+
+ t.integer :role, default: 0
+
+ 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
+end
diff --git a/db/migrate/20230517105713_add_username_to_users.rb b/db/migrate/20230517105713_add_username_to_users.rb
new file mode 100644
index 0000000..545f88c
--- /dev/null
+++ b/db/migrate/20230517105713_add_username_to_users.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddUsernameToUsers < ActiveRecord::Migration[7.0]
+ def change
+ add_column :users, :username, :string
+ end
+end
diff --git a/db/migrate/20230519111424_add_user_to_articles.rb b/db/migrate/20230519111424_add_user_to_articles.rb
new file mode 100644
index 0000000..644df99
--- /dev/null
+++ b/db/migrate/20230519111424_add_user_to_articles.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddUserToArticles < ActiveRecord::Migration[7.0]
+ def change
+ add_reference :articles, :user, null: false, foreign_key: true
+ end
+end
diff --git a/db/migrate/20230519123503_add_views_to_articles.rb b/db/migrate/20230519123503_add_views_to_articles.rb
new file mode 100644
index 0000000..2347d73
--- /dev/null
+++ b/db/migrate/20230519123503_add_views_to_articles.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddViewsToArticles < ActiveRecord::Migration[7.0]
+ def change
+ add_column :articles, :views, :integer, default: 0
+ end
+end
diff --git a/db/migrate/20230519125404_add_user_to_comments.rb b/db/migrate/20230519125404_add_user_to_comments.rb
new file mode 100644
index 0000000..f29c9e2
--- /dev/null
+++ b/db/migrate/20230519125404_add_user_to_comments.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddUserToComments < ActiveRecord::Migration[7.0]
+ def change
+ add_reference :comments, :user, null: false, foreign_key: true
+ end
+end
diff --git a/db/migrate/20230519130033_create_active_storage_tables.active_storage.rb b/db/migrate/20230519130033_create_active_storage_tables.active_storage.rb
new file mode 100644
index 0000000..0c24c0d
--- /dev/null
+++ b/db/migrate/20230519130033_create_active_storage_tables.active_storage.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+# 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 %i[record_type record_id name blob_id], name: :index_active_storage_attachments_uniqueness,
+ unique: true
+ t.foreign_key :active_storage_blobs, column: :blob_id
+ end
+
+ create_table :active_storage_variant_records, id: primary_key_type do |t|
+ t.belongs_to :blob, null: false, index: false, type: foreign_key_type
+ t.string :variation_digest, null: false
+
+ t.index %i[blob_id variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true
+ t.foreign_key :active_storage_blobs, column: :blob_id
+ end
+ end
+
+ private
+
+ def primary_and_foreign_key_types
+ config = Rails.configuration.generators
+ setting = config.options[config.orm][:primary_key_type]
+ primary_key_type = setting || :primary_key
+ foreign_key_type = setting || :bigint
+ [primary_key_type, foreign_key_type]
+ end
+end
diff --git a/db/migrate/20230519130034_create_action_text_tables.action_text.rb b/db/migrate/20230519130034_create_action_text_tables.action_text.rb
new file mode 100644
index 0000000..cd9ea68
--- /dev/null
+++ b/db/migrate/20230519130034_create_action_text_tables.action_text.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# This migration comes from action_text (originally 20180528164100)
+class CreateActionTextTables < ActiveRecord::Migration[6.0]
+ 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 :action_text_rich_texts, id: primary_key_type do |t|
+ t.string :name, null: false
+ t.text :body, size: :long
+ t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
+
+ t.timestamps
+
+ t.index %i[record_type record_id name], name: 'index_action_text_rich_texts_uniqueness', unique: true
+ end
+ end
+
+ private
+
+ def primary_and_foreign_key_types
+ config = Rails.configuration.generators
+ setting = config.options[config.orm][:primary_key_type]
+ primary_key_type = setting || :primary_key
+ foreign_key_type = setting || :bigint
+ [primary_key_type, foreign_key_type]
+ end
+end
diff --git a/db/migrate/20230522122518_create_notifications.rb b/db/migrate/20230522122518_create_notifications.rb
new file mode 100644
index 0000000..f862173
--- /dev/null
+++ b/db/migrate/20230522122518_create_notifications.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class CreateNotifications < ActiveRecord::Migration[7.0]
+ def change
+ create_table :notifications do |t|
+ t.references :recipient, polymorphic: true, null: false
+ t.string :type, null: false
+ t.json :params
+ t.datetime :read_at
+
+ t.timestamps
+ end
+ add_index :notifications, :read_at
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
new file mode 100644
index 0000000..564bbbc
--- /dev/null
+++ b/db/schema.rb
@@ -0,0 +1,111 @@
+# 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.1].define(version: 2023_05_22_122518) do
+ create_table "action_text_rich_texts", force: :cascade do |t|
+ t.string "name", null: false
+ t.text "body"
+ t.string "record_type", null: false
+ t.bigint "record_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
+ end
+
+ create_table "active_storage_attachments", force: :cascade do |t|
+ t.string "name", null: false
+ t.string "record_type", null: false
+ t.bigint "record_id", null: false
+ t.bigint "blob_id", null: false
+ t.datetime "created_at", null: false
+ t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
+ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
+ end
+
+ create_table "active_storage_blobs", force: :cascade 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"
+ t.datetime "created_at", null: false
+ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
+ end
+
+ create_table "active_storage_variant_records", force: :cascade do |t|
+ t.bigint "blob_id", null: false
+ t.string "variation_digest", null: false
+ t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
+ end
+
+ create_table "articles", force: :cascade do |t|
+ t.string "title"
+ t.text "body"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "status"
+ t.integer "user_id", null: false
+ t.integer "views", default: 0
+ t.index ["user_id"], name: "index_articles_on_user_id"
+ end
+
+ create_table "comments", force: :cascade do |t|
+ t.string "commenter"
+ t.text "body"
+ t.integer "article_id", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "status"
+ t.integer "user_id", null: false
+ t.index ["article_id"], name: "index_comments_on_article_id"
+ t.index ["user_id"], name: "index_comments_on_user_id"
+ end
+
+ create_table "notifications", force: :cascade do |t|
+ t.string "recipient_type", null: false
+ t.integer "recipient_id", null: false
+ t.string "type", null: false
+ t.json "params"
+ t.datetime "read_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.index ["read_at"], name: "index_notifications_on_read_at"
+ t.index ["recipient_type", "recipient_id"], name: "index_notifications_on_recipient"
+ end
+
+ create_table "users", force: :cascade do |t|
+ 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.string "confirmation_token"
+ t.datetime "confirmed_at"
+ t.datetime "confirmation_sent_at"
+ t.string "unconfirmed_email"
+ t.integer "role", default: 0
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.string "username"
+ t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
+ 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 "active_storage_attachments", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
+ add_foreign_key "articles", "users"
+ add_foreign_key "comments", "articles"
+ add_foreign_key "comments", "users"
+end
diff --git a/db/seeds.rb b/db/seeds.rb
new file mode 100644
index 0000000..654589e
--- /dev/null
+++ b/db/seeds.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+# 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).
+
+# To drop, create and seed a new local database
+# RUN in terminal rake db:drop db:create db:migrate db:seed
+
+def create_users(number)
+ number.times.map do |i|
+ User.create!(email: "user_email#{i}@email.com", password: 'password', role: Random.new.rand(0..1), created_at: Time.zone.now,
+ updated_at: Time.zone.now, username: "#{i}_username", confirmed_at: Time.zone.now)
+ end
+end
+
+def create_user_with_specific_role
+ # ADMIN
+ User.create!(email: 'adminuser@email.com', password: 'password', role: 2, created_at: Time.zone.now,
+ updated_at: Time.zone.now, username: 'admin_username', confirmed_at: Time.zone.now)
+ # BASIC
+ User.create!(email: 'basicuser@email.com', password: 'password', role: 1, created_at: Time.zone.now,
+ updated_at: Time.zone.now, username: 'basic_username', confirmed_at: Time.zone.now)
+ # GUEST
+ User.create!(email: 'guestuser@email.com', password: 'password', role: 0, created_at: Time.zone.now,
+ updated_at: Time.zone.now, username: 'guest_username', confirmed_at: Time.zone.now)
+end
+
+def create_articles_for_users(users, number)
+ users.map do |user|
+ number.times.map do |i|
+ user.articles.create!(title: "Article #{i}", body: "Body
+ - #{i} - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
+ quis mauris turpis. Curabitur nec cursus lectus. In purus elit, bibendum in
+ odio in, interdum porta neque. Nullam ultricies metus id lobortis interdum.
+ Mauris volutpat nisl ac felis interdum ornare. Aenean interdum sollicitudin
+ risus, et condimentum dolor molestie non. Integer lorem nisi, convallis non
+ pulvinar quis, mollis suscipit sem. Phasellus luctus lacus id lobortis
+ efficitur. Nam hendrerit tempus ante vel rhoncus. Phasellus vulputate odio non
+ risus sodales interdum. Etiam vel bibendum erat. Proin sit amet velit eget leo
+ vestibulum accumsan eu eget ligula. Vestibulum cursus ex tortor, eu semper leo
+ maximus et. Sed turpis neque, sagittis vel mattis nec, tincidunt sit amet
+ purus. Cras eget ullamcorper sem, sed ullamcorper mi. Donec in feugiat dolor.
+ Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac
+ turpis egestas. Quisque non pharetra nulla. Nam libero lacus, interdum ac
+ viverra ac, tempus ac lorem. In venenatis quis nulla quis pretium. Nam
+ placerat, arcu a euismod dignissim, nisi orci bibendum neque, eget venenatis
+ nisl sapien vel mi. Quisque quis nisl vitae mi faucibus dapibus eget ac ipsum.
+ Aliquam vehicula dolor blandit volutpat efficitur. Nulla eu nunc purus.
+ Interdum et malesuada fames ac ante ipsum primis in faucibus. Quisque iaculis
+ venenatis tellus id pharetra. Phasellus at aliquam erat, id finibus est.",
+ status: 'public', created_at: Time.zone.now, updated_at: Time.zone.now, user_id: i)
+ end
+ end
+end
+
+def create_comments_for_articles(articles, users, number)
+ articles.map do |article|
+ comment_users = users.reject { |user| user == article.user }
+ number.times.map do |i|
+ article.comments.create!(user: comment_users.sample, body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris
+ quis mauris turpis. Curabitur nec cursus lectus. In purus elit, bibendum in
+ odio in, interdum porta neque. Nullam ultricies metus id lobortis interdum.
+ Mauris volutpat nisl ac felis interdum ornare. Aenean interdum sollicitudin
+ risus, et condimentum dolor molestie non.", article_id: i, created_at: Time.zone.now, updated_at: Time.zone.now)
+ end
+ end
+end
+
+def setup
+ create_user_with_specific_role
+ new_users = create_users(10)
+ new_articles = create_articles_for_users(new_users, 5).flatten
+ create_comments_for_articles(new_articles, new_users, 10)
+end
+
+setup
diff --git a/db/test.sqlite3-shm b/db/test.sqlite3-shm
new file mode 100644
index 0000000..97f5fa3
Binary files /dev/null and b/db/test.sqlite3-shm differ
diff --git a/db/test.sqlite3-wal b/db/test.sqlite3-wal
new file mode 100644
index 0000000..e429143
Binary files /dev/null and b/db/test.sqlite3-wal differ
diff --git a/lib/.DS_Store b/lib/.DS_Store
new file mode 100644
index 0000000..6d9d37c
Binary files /dev/null and b/lib/.DS_Store differ
diff --git a/lib/assets/.keep b/lib/assets/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/lib/tasks/.keep b/lib/tasks/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/log/.keep b/log/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/public/404.html b/public/404.html
new file mode 100644
index 0000000..2be3af2
--- /dev/null
+++ b/public/404.html
@@ -0,0 +1,67 @@
+
+
+
+ The page you were looking for doesn't exist (404)
+
+
+
+
+
+
+
+
+
The page you were looking for doesn't exist.
+
You may have mistyped the address or the page may have moved.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/422.html b/public/422.html
new file mode 100644
index 0000000..c08eac0
--- /dev/null
+++ b/public/422.html
@@ -0,0 +1,67 @@
+
+
+
+ The change you wanted was rejected (422)
+
+
+
+
+
+
+
+
+
The change you wanted was rejected.
+
Maybe you tried to change something you didn't have access to.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/500.html b/public/500.html
new file mode 100644
index 0000000..78a030a
--- /dev/null
+++ b/public/500.html
@@ -0,0 +1,66 @@
+
+
+
+ We're sorry, but something went wrong (500)
+
+
+
+
+
+
+
+
+
We're sorry, but something went wrong.
+
+
If you are the application owner check the logs for more information.
+
+
+
diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png
new file mode 100644
index 0000000..e69de29
diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
new file mode 100644
index 0000000..e69de29
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..c19f78a
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1 @@
+# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
diff --git a/spec/.DS_Store b/spec/.DS_Store
new file mode 100644
index 0000000..a5aaf05
Binary files /dev/null and b/spec/.DS_Store differ
diff --git a/spec/concerns/visible_spec.rb b/spec/concerns/visible_spec.rb
new file mode 100644
index 0000000..add8a09
--- /dev/null
+++ b/spec/concerns/visible_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Visible do
+ let(:article) { create(:article) }
+
+ describe '#archived?' do
+ context 'when article status is archived' do
+ it 'returns true' do
+ article.status = 'archived'
+ expect(article.archived?).to be true
+ end
+ end
+
+ context 'when article status is not archived' do
+ it 'returns false' do
+ article.status = 'public'
+ expect(article.archived?).to be false
+ end
+ end
+
+ describe '#public_count' do
+ before do
+ create(:article)
+ end
+
+ it 'returns to article count' do
+ expect(Article.public_count).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/controllers/.DS_Store b/spec/controllers/.DS_Store
new file mode 100644
index 0000000..bffebe8
Binary files /dev/null and b/spec/controllers/.DS_Store differ
diff --git a/spec/controllers/admins_controller_spec.rb b/spec/controllers/admins_controller_spec.rb
new file mode 100644
index 0000000..7848b3e
--- /dev/null
+++ b/spec/controllers/admins_controller_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe AdminsController, type: :controller do
+ let(:admin_user) { create(:admin) }
+
+ describe 'GET #index' do
+ context 'with admin user' do
+ before do
+ sign_in admin_user
+ end
+
+ it 'renders the :admin template' do
+ get :home
+ expect(response).to render_template(:home)
+ end
+ end
+
+ context 'when a non admin user' do
+ it 'redirects to root route' do
+ get :home
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/articles/admin_user_article_controller_spec.rb b/spec/controllers/articles/admin_user_article_controller_spec.rb
new file mode 100644
index 0000000..df78c61
--- /dev/null
+++ b/spec/controllers/articles/admin_user_article_controller_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ArticlesController, type: :controller do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in admin
+ end
+ # TODO
+ # Admin can do same as 'basic' user plus edit/delete any article & comment
+ # Delete any article
+ # Delete any comment
+ # Admin dashboard template
+ # Edit other users details
+
+ describe '"Admin" user' do
+ describe 'GET #new' do
+ it 'renders :new template' do
+ get :new
+ expect(response).to render_template(:new)
+ end
+
+ it 'assigns new Article to @article' do
+ get :new
+ expect(assigns(:article)).to be_a_new(Article)
+ end
+ end
+
+ describe 'POST #create' do
+ context 'with valid params' do
+ let(:params) do
+ {
+ article: {
+ title: 'New article title',
+ body: 'New article body',
+ status: 'public',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ admin:
+ }
+ }
+ end
+
+ it 'creates an article' do
+ expect { post :create, params: }.to change(Article, :count).by(1)
+ end
+
+ it 'redirects to article :show template' do
+ post(:create, params:)
+ expect(response).to redirect_to(article_path(assigns[:article]))
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) do
+ {
+ article: {
+ title: '',
+ body: 'New article body',
+ status: 'public',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ admin:
+ }
+ }
+ end
+
+ it 'does not create an article' do
+ expect { post :create, params: }.not_to change(Article, :count)
+ end
+
+ it 'renders :new template' do
+ post(:create, params:)
+ expect(response).to render_template(:new)
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ describe 'GET #edit' do
+ let(:article) { create(:article) }
+
+ it 'renders :edit template' do
+ get :edit, params: { id: article.id }
+ expect(response).to render_template(:edit)
+ end
+
+ it 'assigns the article to @article' do
+ get :edit, params: { id: article.id }
+ expect(assigns(:article)).to eq(article)
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:article) { create(:article) }
+
+ context 'with valid params' do
+ let(:params) { attributes_for(:article, title: 'new title') }
+
+ it 'renders :show template' do
+ put :update, params: { id: article.id, article: params }
+ expect(response).to redirect_to(article)
+ end
+
+ it 'update article value' do
+ put :update, params: { id: article.id, article: params }
+ article.reload
+ expect(article.title).to eq('new title')
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) { attributes_for(:article, title: ' ', body: 'new body text') }
+
+ it 'renders :edit template' do
+ put :update, params: { id: article.id, article: params }
+ expect(response).to render_template(:edit)
+ end
+
+ it 'does not update article value' do
+ put :update, params: { id: article.id, article: params }
+ article.reload
+ expect(article.title).not_to eq(' ')
+ end
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let(:article) { create(:article) }
+
+ it 'redirects to :index template' do
+ delete :destroy, params: { id: article.id }
+ expect(response).to redirect_to(articles_path)
+ end
+
+ it 'deletes article' do
+ delete :destroy, params: { id: article.id }
+ expect(Article).not_to exist(article.id)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/articles/basic_user_article_controller_spec.rb b/spec/controllers/articles/basic_user_article_controller_spec.rb
new file mode 100644
index 0000000..7404be7
--- /dev/null
+++ b/spec/controllers/articles/basic_user_article_controller_spec.rb
@@ -0,0 +1,139 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ArticlesController, type: :controller do
+ let(:basic) { create(:basic) }
+
+ before do
+ sign_in basic
+ end
+
+ describe '"Basic" user' do
+ describe 'GET #new' do
+ it 'renders :new template' do
+ get :new
+ expect(response).to render_template(:new)
+ end
+
+ it 'assigns new Article to @article' do
+ get :new
+ expect(assigns(:article)).to be_a_new(Article)
+ end
+ end
+
+ describe 'POST #create' do
+ context 'with valid params' do
+ let(:params) do
+ {
+ article: {
+ title: 'New article title',
+ body: 'New article body',
+ status: 'public',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ basic:
+ }
+ }
+ end
+
+ it 'creates an article' do
+ expect { post :create, params: }.to change(Article, :count).by(1)
+ end
+
+ it 'redirects to article :show template' do
+ post(:create, params:)
+ expect(response).to redirect_to(article_path(assigns[:article]))
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) do
+ {
+ article: {
+ title: '',
+ body: 'New article body',
+ status: 'public',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now,
+ basic:
+ }
+ }
+ end
+
+ it 'does not create an article' do
+ expect { post :create, params: }.not_to change(Article, :count)
+ end
+
+ it 'renders :new template' do
+ post(:create, params:)
+ expect(response).to render_template(:new)
+ expect(response).to have_http_status(422)
+ end
+ end
+ end
+
+ describe 'GET #edit' do
+ let(:article) { create(:article) }
+
+ it 'renders :edit template' do
+ get :edit, params: { id: article.id }
+ expect(response).to render_template(:edit)
+ end
+
+ it 'assigns the article to @article' do
+ get :edit, params: { id: article.id }
+ expect(assigns(:article)).to eq(article)
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:article) { create(:article) }
+
+ context 'with valid params' do
+ let(:params) { attributes_for(:article, title: 'new title') }
+
+ it 'renders :show template' do
+ put :update, params: { id: article.id, article: params }
+ expect(response).to redirect_to(article)
+ end
+
+ it 'update article value' do
+ put :update, params: { id: article.id, article: params }
+ article.reload
+ expect(article.title).to eq('new title')
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) { attributes_for(:article, title: ' ', body: 'new body text') }
+
+ it 'renders :edit template' do
+ put :update, params: { id: article.id, article: params }
+ expect(response).to render_template(:edit)
+ end
+
+ it 'does not update article value' do
+ put :update, params: { id: article.id, article: params }
+ article.reload
+ expect(article.title).not_to eq(' ')
+ end
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let(:article) { create(:article) }
+
+ it 'redirects to :index template' do
+ delete :destroy, params: { id: article.id }
+ expect(response).to redirect_to(articles_path)
+ end
+
+ it 'deletes article' do
+ delete :destroy, params: { id: article.id }
+ expect(Article).not_to exist(article.id)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/articles/guest_user_article_controller_spec.rb b/spec/controllers/articles/guest_user_article_controller_spec.rb
new file mode 100644
index 0000000..106a654
--- /dev/null
+++ b/spec/controllers/articles/guest_user_article_controller_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe ArticlesController, type: :controller do
+ describe '"Guest" user' do
+ describe 'GET #show' do
+ let(:article) { create(:article) }
+
+ it 'renders :show template' do
+ get :show, params: { id: article.id }
+ expect(response).to render_template(:show)
+ end
+
+ it 'assigns requested article to @article' do
+ get :show, params: { id: article.id }
+ expect(assigns(:article)).to eq(article)
+ end
+ end
+
+ describe 'GET #index' do
+ it 'renders :index template' do
+ get :index
+ expect(response).to render_template(:index)
+ expect(response).to have_http_status(200)
+ end
+
+ it 'assigns articles to template' do
+ get :index
+ expect(assigns(:articles)).to match_array(Article.all)
+ end
+ end
+
+ describe 'GET #new' do
+ it 'redirects to :sign_in template' do
+ get :new
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ describe 'POST #create' do
+ let(:params) do
+ {
+ article: {
+ title: 'New article title',
+ body: 'New article body',
+ status: 'public',
+ created_at: Time.zone.now,
+ updated_at: Time.zone.now
+ }
+ }
+ end
+
+ it 'redirects to :sign_in template' do
+ post(:create, params:)
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ describe 'GET #edit' do
+ let(:article) { create(:article) }
+
+ it 'redirects to :sign_in template' do
+ get :edit, params: { id: article.id }
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ describe 'PUT #update' do
+ let(:article) { create(:article) }
+ let(:params) { attributes_for(:article, title: 'new title') }
+
+ it 'redirects to :sign_in template' do
+ put :update, params: { id: article.id, article: params }
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let(:article) { create(:article) }
+
+ it 'redirects to :sign_in template' do
+ delete :destroy, params: { id: article.id }
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/users/registrations_controller_spec.rb b/spec/controllers/users/registrations_controller_spec.rb
new file mode 100644
index 0000000..d7150d7
--- /dev/null
+++ b/spec/controllers/users/registrations_controller_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Users::RegistrationsController, type: :controller do
+ # Use Devise helpers for authentication
+ include Devise::Test::ControllerHelpers
+
+ let(:admin) { create(:user, role: 'admin') }
+ let(:user) { create(:user, role: 'basic') }
+ let(:other_user) { create(:user, role: 'basic') }
+
+ before do
+ # Specify that we're using the custom Devise routes
+ @request.env['devise.mapping'] = Devise.mappings[:user] # rubocop:disable RSpec/InstanceVariable
+ end
+
+ describe 'GET #edit' do
+ context 'when admin is logged in' do
+ before { sign_in admin }
+
+ it 'allows admin to edit any user profile' do
+ get :edit, params: { id: user.id }
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:user)).to eq(user)
+ end
+ end
+
+ context 'when regular user is logged in' do
+ before { sign_in user }
+
+ it 'allows user to edit their own profile' do
+ get :edit, params: { id: user.id }
+ expect(response).to have_http_status(:ok)
+ expect(assigns(:user)).to eq(user)
+ end
+
+ it 'redirects user when attempting to edit another user’s profile' do
+ get :edit, params: { id: other_user.id }
+ expect(response).to redirect_to(root_path)
+ expect(flash[:alert]).to eq('Access denied.')
+ end
+ end
+ end
+
+ describe 'PUT #update' do
+ context 'when admin is logged in' do
+ before { sign_in admin }
+
+ it 'allows admin to update any user profile' do
+ put :update, params: { id: user.id, user: { username: 'NewUsername' } }
+ expect(response).to redirect_to(edit_user_account_path(user))
+ expect(flash[:notice]).to eq('User updated successfully.')
+ user.reload
+ expect(user.username).to eq('NewUsername')
+ end
+ end
+
+ context 'when regular user is logged in' do
+ before { sign_in user }
+
+ it 'allows user to update their own profile' do
+ put :update, params: { id: user.id, user: { username: 'UpdatedUsername' } }
+ expect(response).to redirect_to(edit_user_registration_path)
+ expect(flash[:notice]).to eq('User updated successfully.')
+ user.reload
+ expect(user.username).to eq('UpdatedUsername')
+ end
+
+ it 'redirects user when attempting to update another user’s profile' do
+ put :update, params: { id: other_user.id, user: { username: 'HackedUsername' } }
+ expect(response).to redirect_to(root_path)
+ expect(flash[:alert]).to eq('Access denied.')
+ other_user.reload
+ expect(other_user.username).not_to eq('HackedUsername')
+ end
+ end
+ end
+end
diff --git a/spec/factories/articles.rb b/spec/factories/articles.rb
new file mode 100644
index 0000000..476a3a4
--- /dev/null
+++ b/spec/factories/articles.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: articles
+#
+# id :integer not null, primary key
+# title :string
+# body :text
+# created_at :datetime not null
+# updated_at :datetime not null
+# status :string
+# user_id :integer not null
+# views :integer default(0)
+#
+FactoryBot.define do
+ factory :article do
+ title { Faker::Lorem.words(number: 2) }
+ body { Faker::Lorem.paragraphs }
+ created_at { Faker::Time.between(from: DateTime.now - 1, to: DateTime.now) }
+ updated_at { Faker::Time.between(from: DateTime.now - 1, to: DateTime.now) }
+ status { 'public' }
+ user factory: %i[user]
+ end
+end
diff --git a/spec/factories/user.rb b/spec/factories/user.rb
new file mode 100644
index 0000000..033870b
--- /dev/null
+++ b/spec/factories/user.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :user do
+ email { Faker::Internet.email }
+ password { 'password' }
+ password_confirmation { 'password' }
+ created_at { Time.zone.local(2024) }
+ updated_at { Time.zone.local(2024) }
+ confirmed_at { Time.zone.now }
+ factory :guest do
+ role { 0 } # User: guest
+ end
+
+ factory :basic do
+ role { 1 } # User: basic
+ end
+
+ factory :admin do
+ role { 2 } # User: admin
+ end
+ end
+end
diff --git a/spec/models/article_spec.rb b/spec/models/article_spec.rb
new file mode 100644
index 0000000..b638510
--- /dev/null
+++ b/spec/models/article_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: articles
+#
+# id :integer not null, primary key
+# title :string
+# body :text
+# created_at :datetime not null
+# updated_at :datetime not null
+# status :string
+# user_id :integer not null
+# views :integer default(0)
+#
+require 'rails_helper'
+
+RSpec.describe Article, type: :model do
+ let(:user) { create(:user) }
+
+ context 'with invalid params' do
+ let(:article) { described_class.new(user:, title: 'title 1', body: 'body text and content', status: 'public') }
+ let(:errors) { article.errors.messages }
+
+ it 'is invalid without a user' do
+ article.user = nil
+ expect(article).not_to be_valid
+ end
+
+ it 'is invalid without a title' do
+ article.title = nil
+ expect(article).not_to be_valid
+ expect(errors[:title]).to include "can't be blank"
+ expect(errors[:title]).to include 'is too short (minimum is 1 character)'
+ end
+
+ it 'is invalid without a body' do
+ article.body = nil
+ expect(article).not_to be_valid
+ expect(errors[:body]).to include "can't be blank"
+ end
+
+ it 'is invalid when body is less than 10 characters' do
+ article.body = 'text'
+ expect(article).not_to be_valid
+ expect(errors[:body]).to include 'is too short (minimum is 10 characters)'
+ end
+
+ it 'is invalid without a status' do
+ article.status = nil
+ expect(article).not_to be_valid
+ expect(errors[:status]).to include 'is not included in the list'
+ end
+ end
+
+ context 'with valid params' do
+ it 'is valid' do
+ article = described_class.new(user:, title: 'title 1', body: 'body text and content', status: 'public')
+ expect(article).to be_valid
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
new file mode 100644
index 0000000..48bf95b
--- /dev/null
+++ b/spec/models/user_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+# == Schema Information
+#
+# Table name: users
+#
+# id :integer not null, primary key
+# email :string default(""), not null
+# encrypted_password :string default(""), not null
+# reset_password_token :string
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# confirmation_token :string
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string
+# role :integer default("guest")
+# created_at :datetime not null
+# updated_at :datetime not null
+# username :string
+#
+# User model validations are normally covered in Devise, however adding them in anyway for coverage of #create
+RSpec.describe User do
+ let(:user) { create(:user) }
+ let(:errors) { user.errors.messages }
+
+ describe '#create' do
+ context 'with all values present' do
+ it 'creates a new user' do
+ expect(user).to be_valid
+ end
+ end
+
+ context 'with values missing' do
+ it 'raises an error when email is missing' do
+ user.email = ''
+ expect(user).not_to be_valid
+ end
+
+ it 'raises an error with password missing' do
+ user.password = ''
+ expect(user).not_to be_valid
+ expect(errors[:password]).to include "can't be blank"
+ end
+
+ it 'raises an error with password and confirmation not matching' do
+ user.password = 'pass'
+ user.password_confirmation = 'diffpass'
+ expect(user).not_to be_valid
+ expect(errors[:password_confirmation]).to include "doesn't match Password"
+ end
+ end
+ end
+end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 0000000..0f94289
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+require 'spec_helper'
+ENV['RAILS_ENV'] ||= 'test'
+require_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!
+require_relative 'support/factory_bot'
+require_relative 'support/chrome'
+require 'devise'
+
+if ENV['RAILS_ENV'] == 'test'
+ require 'simplecov'
+ SimpleCov.start 'rails'
+ puts 'required simplecov'
+end
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are
+# run as spec files by default. This means that files in spec/support that end
+# in _spec.rb will both be required and run as specs, causing the specs to be
+# run twice. It is recommended that you do not name files matching this glob to
+# end with _spec.rb. You can configure this pattern with the --pattern
+# option on the command line or in ~/.rspec, .rspec or `.rspec-local`.
+#
+# The following line is provided for convenience purposes. It has the downside
+# of increasing the boot-up time by auto-requiring all files in the support
+# directory. Alternatively, in the individual `*_spec.rb` files, manually
+# require only the support files necessary.
+#
+# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
+
+# Checks for pending migrations and applies them before tests are run.
+# If you are not using ActiveRecord, you can remove these lines.
+begin
+ ActiveRecord::Migration.maintain_test_schema!
+rescue ActiveRecord::PendingMigrationError => e
+ puts e.to_s.strip
+ exit 1
+end
+
+RSpec.configure do |config|
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_path = Rails.root.join('spec/fixtures').to_s
+
+ config.include Devise::Test::ControllerHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :view
+ config.include Devise::Test::IntegrationHelpers, type: :feature
+ config.include Devise::Test::IntegrationHelpers, type: :request
+
+ config.formatter = :documentation
+
+ # 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://relishapp.com/rspec/rspec-rails/docs
+ 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")
+
+ # Clean database before test run
+ config.before(:suite) do
+ DatabaseCleaner.clean_with(:truncation) # Clean the database completely before the test suite
+ DatabaseCleaner.strategy = :transaction # Use transactions during individual tests
+ end
+
+ config.around do |example|
+ DatabaseCleaner.cleaning { example.run }
+ end
+end
diff --git a/spec/requests/articles_spec.rb b/spec/requests/articles_spec.rb
new file mode 100644
index 0000000..93141c9
--- /dev/null
+++ b/spec/requests/articles_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Articles', type: :request do
+ let(:admin_user) { create(:admin) }
+ let(:guest_user) { create(:guest) }
+ let(:basic_user) { create(:basic) }
+
+ describe '#home' do
+ context 'with admin user' do
+ before do
+ sign_in admin_user
+ end
+
+ it 'renders the users home page' do
+ get root_path
+ expect(response).to have_http_status(:successful)
+ end
+ end
+
+ context 'with basic user' do
+ before do
+ sign_in basic_user
+ end
+
+ it 'renders the users home page' do
+ get root_path
+ expect(response).to have_http_status(:successful)
+ end
+ end
+
+ context 'with guest user' do
+ before do
+ sign_in guest_user
+ end
+
+ it 'renders the users home page' do
+ get root_path
+ expect(response).to have_http_status(:successful)
+ end
+ end
+
+ context 'with no user signed in' do
+ it 'redirects to the sign in page' do
+ get root_path
+ expect(response).to redirect_to user_session_path
+ end
+ end
+ end
+end
diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb
new file mode 100644
index 0000000..bcc0b29
--- /dev/null
+++ b/spec/requests/users_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'Users', type: :request do
+ let(:admin_user) { create(:admin) }
+ let(:guest_user) { create(:guest) }
+ let(:basic_user) { create(:basic) }
+
+ describe 'GET /index' do
+ context 'when logged in as admin user' do
+ before do
+ sign_in admin_user
+ end
+
+ it 'renders admin homepage' do
+ get home_admins_path
+ expect(response).to have_http_status(:successful)
+ expect(response).to render_template :home
+ end
+ end
+
+ context 'when logged in as guest user' do
+ before do
+ sign_in guest_user
+ end
+
+ it 'redirects to the sign in page' do
+ get home_admins_path
+ expect(response).to have_http_status(:redirect)
+ expect(response).to redirect_to user_session_path
+ end
+ end
+
+ context 'when logged in as basic user' do
+ before do
+ sign_in basic_user
+ end
+
+ it 'redirects to the sign in' do
+ get home_admins_path
+ expect(response).to have_http_status(:redirect)
+ expect(response).to redirect_to user_session_path
+ end
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
new file mode 100644
index 0000000..336858f
--- /dev/null
+++ b/spec/spec_helper.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require 'factory_bot_rails'
+# 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://relishapp.com/rspec/rspec-core/docs/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
+
+ # Allow for FactoryBot shorthand in tests = "create" instead of "FactoryBot.create"
+ config.include FactoryBot::Syntax::Methods
+end
diff --git a/spec/support/chrome.rb b/spec/support/chrome.rb
new file mode 100644
index 0000000..621b426
--- /dev/null
+++ b/spec/support/chrome.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before(:each, type: :system) do
+ if ENV['SHOW_BROWSER'] == 'true'
+ driven_by :selenium_chrome
+ else
+ driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
+ end
+ end
+end
diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb
new file mode 100644
index 0000000..2e7665c
--- /dev/null
+++ b/spec/support/factory_bot.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.include FactoryBot::Syntax::Methods
+end
diff --git a/spec/views/users/index_spec.rb b/spec/views/users/index_spec.rb
new file mode 100644
index 0000000..d01eac9
--- /dev/null
+++ b/spec/views/users/index_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'users/index.html.erb', type: :view do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/storage/.keep b/storage/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/.keep b/vendor/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/vendor/javascript/.keep b/vendor/javascript/.keep
new file mode 100644
index 0000000..e69de29