diff --git a/Gemfile.lock b/Gemfile.lock index ac898c77c..66613187f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -233,6 +233,8 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + tzinfo-data (1.2023.4) + tzinfo (>= 1.0.0) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb new file mode 100644 index 000000000..c95e8a79d --- /dev/null +++ b/app/controllers/articles_controller.rb @@ -0,0 +1,108 @@ +class ArticlesController < ApplicationController + before_action :set_article, only: %i[ show edit update destroy ] + + # GET /articles or /articles.json + def index + @articles = Article.all + + # Default values + sort_column = params[:sort] || 'title' + sort_order = params[:order] || 'asc' + + # Toggle the order if sorting on the same column + sort_order = toggle_order(sort_order) if sort_column == params[:sort] + + # Sorting logic based on sort_column and sort_order + case sort_column + when 'title' + @articles = @articles.order(title: sort_order) + when 'author' + @articles = @articles.order(author: sort_order) + when 'date' + @articles = @articles.order(date: sort_order) + end + end + + # GET /articles/search + def search + @query = params[:query] + @results = Article.search(@query) + + if @query.present? + if @results&.any? + @articles = @results + else + @articles = [] + end + else + @articles = Article.all + end + end + + # GET /articles/1 or /articles/1.json + def show + end + + # GET /articles/new + def new + @article = Article.new + end + + # GET /articles/1/edit + def edit + end + + # POST /articles or /articles.json + def create + @article = Article.new(article_params) + + respond_to do |format| + if @article.save + format.html { redirect_to article_url(@article), notice: "Article was successfully created." } + format.json { render :show, status: :created, location: @article } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @article.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /articles/1 or /articles/1.json + def update + respond_to do |format| + if @article.update(article_params) + format.html { redirect_to article_url(@article), notice: "Article was successfully updated." } + format.json { render :show, status: :ok, location: @article } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @article.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /articles/1 or /articles/1.json + def destroy + @article.destroy! + + respond_to do |format| + format.html { redirect_to articles_url, notice: "Article was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_article + @article = Article.find(params[:id]) + end + + # Toggle between ascending order and descending order + def toggle_order(order) + order == 'asc' ? 'desc' : 'asc' + end + + # Only allow a list of trusted parameters through. + def article_params + params.require(:article).permit(:title, :content, :author, :date) + end +end diff --git a/app/controllers/pages_controller.rb b/app/controllers/pages_controller.rb new file mode 100644 index 000000000..f5680a0c3 --- /dev/null +++ b/app/controllers/pages_controller.rb @@ -0,0 +1,7 @@ +class PagesController < ApplicationController + def home + end + + def about + end +end diff --git a/app/helpers/articles_helper.rb b/app/helpers/articles_helper.rb new file mode 100644 index 000000000..296827759 --- /dev/null +++ b/app/helpers/articles_helper.rb @@ -0,0 +1,2 @@ +module ArticlesHelper +end diff --git a/app/helpers/pages_helper.rb b/app/helpers/pages_helper.rb new file mode 100644 index 000000000..2c057fd05 --- /dev/null +++ b/app/helpers/pages_helper.rb @@ -0,0 +1,2 @@ +module PagesHelper +end diff --git a/app/models/article.rb b/app/models/article.rb new file mode 100644 index 000000000..df77f7c2c --- /dev/null +++ b/app/models/article.rb @@ -0,0 +1,9 @@ +class Article < ApplicationRecord + validates :title, presence: true + validates :content, presence: true + + # perform case-insentitive search on title or content of articles + def self.search(query) + where("title LIKE ? OR content LIKE ?", "%#{query}%", "%#{query}%") + end +end diff --git a/app/views/articles/_article.html.erb b/app/views/articles/_article.html.erb new file mode 100644 index 000000000..15f315457 --- /dev/null +++ b/app/views/articles/_article.html.erb @@ -0,0 +1,16 @@ +
+ +
+
+ By <%= article.author %> +
+
+
<%= article.title %>
+

<%= article.content %>

+
+ +
+ +
diff --git a/app/views/articles/_article.json.jbuilder b/app/views/articles/_article.json.jbuilder new file mode 100644 index 000000000..2e17e23ea --- /dev/null +++ b/app/views/articles/_article.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! article, :id, :title, :content, :author, :date, :created_at, :updated_at +json.url article_url(article, format: :json) diff --git a/app/views/articles/_form.html.erb b/app/views/articles/_form.html.erb new file mode 100644 index 000000000..e41f060c2 --- /dev/null +++ b/app/views/articles/_form.html.erb @@ -0,0 +1,38 @@ +<%= form_with(model: article) do |form| %> + <% if article.errors.any? %> +
+

<%= pluralize(article.errors.count, "error") %> prohibited this article from being saved:

+ + +
+ <% end %> + +
+ <%= form.text_field :author, class:"form-control", placeholder:"Author" %> +
+ +
+ <%= form.text_field :title, class:"form-control", placeholder:"Title" %> +
+ +
+ <%= form.text_area :content, class:"form-control", placeholder:"Fill with your content" %> +
+ +
+ <%= form.date_field :date, id: :article_date, class:"form-control", value:Date.today, type: :hidden %> +
+ + <%#
%> + <%# <%= form.text_field :user_id, id: :article_user_id, class:"form-control", value:current_user.id, type: :hidden %> + <%# %> + <%#
%> + +
+ <%= form.submit "Save", class:"btn btn-primary" %> +
+<% end %> diff --git a/app/views/articles/_table.html.erb b/app/views/articles/_table.html.erb new file mode 100644 index 000000000..1d29a1d73 --- /dev/null +++ b/app/views/articles/_table.html.erb @@ -0,0 +1,24 @@ + + + + + + + + + + <% if @articles.present? %> + <% @articles.each do |article| %> + + + + + + <% end %> + <% else %> + + + + <% end %> + +
<%= link_to "Title", articles_path(sort: 'title') %><%= link_to "Author", articles_path(sort: 'author') %><%= link_to "Date Posted", articles_path(sort: 'date') %>
<%= link_to "#{article.title}", article %><%=article.author %><%=article.date %>
No articles found.
\ No newline at end of file diff --git a/app/views/articles/edit.html.erb b/app/views/articles/edit.html.erb new file mode 100644 index 000000000..fd7c7391e --- /dev/null +++ b/app/views/articles/edit.html.erb @@ -0,0 +1,10 @@ +

Editing article

+ +<%= render "form", article: @article %> + +
+ +
+ <%= link_to "Details...", @article, class:"btn btn-primary" %> + <%= link_to "Back", articles_path, class:"btn btn-dark" %> +
diff --git a/app/views/articles/index.html.erb b/app/views/articles/index.html.erb new file mode 100644 index 000000000..638fccbc6 --- /dev/null +++ b/app/views/articles/index.html.erb @@ -0,0 +1,5 @@ +

Articles

+ +<%= render "table" %> + +<%= link_to "New article", new_article_path, class:"btn btn-secondary" %> diff --git a/app/views/articles/index.json.jbuilder b/app/views/articles/index.json.jbuilder new file mode 100644 index 000000000..09a21f390 --- /dev/null +++ b/app/views/articles/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @articles, partial: "articles/article", as: :article diff --git a/app/views/articles/new.html.erb b/app/views/articles/new.html.erb new file mode 100644 index 000000000..f63a2ce2f --- /dev/null +++ b/app/views/articles/new.html.erb @@ -0,0 +1,9 @@ +

New article

+ +<%= render "form", article: @article %> + +
+ +
+ <%= link_to "Back", articles_path, class:"btn btn-dark" %> +
diff --git a/app/views/articles/search.html.erb b/app/views/articles/search.html.erb new file mode 100644 index 000000000..638fccbc6 --- /dev/null +++ b/app/views/articles/search.html.erb @@ -0,0 +1,5 @@ +

Articles

+ +<%= render "table" %> + +<%= link_to "New article", new_article_path, class:"btn btn-secondary" %> diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb new file mode 100644 index 000000000..69b5f09a4 --- /dev/null +++ b/app/views/articles/show.html.erb @@ -0,0 +1,8 @@ +<%= render @article %> + +
+ <%= link_to "Edit", edit_article_path(@article) , class:"btn btn-secondary" %> + <%= link_to "Back", articles_path, class:"btn btn-dark" %> + + <%= button_to "Delete", @article, method: :delete, class:"btn btn-danger" %> +
diff --git a/app/views/articles/show.json.jbuilder b/app/views/articles/show.json.jbuilder new file mode 100644 index 000000000..dd1fbb4c4 --- /dev/null +++ b/app/views/articles/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "articles/article", article: @article diff --git a/app/views/layouts/_navbar.html.erb b/app/views/layouts/_navbar.html.erb new file mode 100644 index 000000000..0ae180270 --- /dev/null +++ b/app/views/layouts/_navbar.html.erb @@ -0,0 +1,26 @@ + + diff --git a/app/views/layouts/_searchForm.html.erb b/app/views/layouts/_searchForm.html.erb new file mode 100644 index 000000000..2106a1087 --- /dev/null +++ b/app/views/layouts/_searchForm.html.erb @@ -0,0 +1,4 @@ +<%= form_tag(article_search_path, method: 'get', class: "d-flex") do %> + <%= text_field_tag(:query, params[:query], class: "form-control me-2", placeholder: 'Search articles...') %> + <%= submit_tag('Search', class: "btn btn-outline-success") %> +<% end %> \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 812bfb90f..b3c008c12 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -8,9 +8,12 @@ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> + + <%= render 'layouts/navbar' %> <%= yield %> + diff --git a/app/views/pages/about.html.erb b/app/views/pages/about.html.erb new file mode 100644 index 000000000..b2cec85f6 --- /dev/null +++ b/app/views/pages/about.html.erb @@ -0,0 +1,4 @@ +
+

Shopify Encyclopedia

+

Shopify Encyclopedia is the place to be for all the gist within Shopify. For non-employees, you wold be able to have some transparency on our company mission and values

+
\ No newline at end of file diff --git a/app/views/pages/home.html.erb b/app/views/pages/home.html.erb new file mode 100644 index 000000000..bc8501234 --- /dev/null +++ b/app/views/pages/home.html.erb @@ -0,0 +1,4 @@ +
+

Welcome to Shopify Encyclopedia

+

Feel free to browse

+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index a125ef085..eb49abdc0 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,10 +1,14 @@ Rails.application.routes.draw do # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html - + resources :articles + + get '/about', to: 'pages#about' + get '/articles/search', to: 'articles#search', as: 'article_search' + # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. # Can be used by load balancers and uptime monitors to verify that the app is live. get "up" => "rails/health#show", as: :rails_health_check # Defines the root path route ("/") - # root "posts#index" + root "pages#home" end diff --git a/db/migrate/20240125082905_create_articles.rb b/db/migrate/20240125082905_create_articles.rb new file mode 100644 index 000000000..84add4e46 --- /dev/null +++ b/db/migrate/20240125082905_create_articles.rb @@ -0,0 +1,12 @@ +class CreateArticles < ActiveRecord::Migration[7.1] + def change + create_table :articles do |t| + t.string :title + t.text :content + t.string :author + t.date :date + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 000000000..f3918f0a2 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,23 @@ +# 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: 2024_01_25_082905) do + create_table "articles", force: :cascade do |t| + t.string "title" + t.text "content" + t.string "author" + t.date "date" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + +end diff --git a/test/controllers/articles_controller_test.rb b/test/controllers/articles_controller_test.rb new file mode 100644 index 000000000..6fe159b4a --- /dev/null +++ b/test/controllers/articles_controller_test.rb @@ -0,0 +1,53 @@ +require "test_helper" + +class ArticlesControllerTest < ActionDispatch::IntegrationTest + setup do + @article = Article.create( + title: 'First Post', + content: 'My first article for Shopify', + author: 'Maria Stringy', + date: Date.today + ) + end + + test "should get index" do + get articles_url + assert_response :success, 'Failed to get the index page.' + end + + test "should get new" do + get new_article_url + assert_response :success, 'Failed to get the new article page.' + end + + test "should create article" do + assert_difference("Article.count") do + post articles_url, params: { article: { author: @article.author, content: @article.content, date: @article.date, title: @article.title } } + end + + assert_redirected_to article_url(Article.last), 'Article was not successfully created.' + end + + test "should show article" do + get article_url(@article) + assert_response :success, 'Failed to get the article show page.' + end + + test "should get edit" do + get edit_article_url(@article) + assert_response :success, 'Failed to get the edit article page.' + end + + test "should update article" do + patch article_url(@article), params: { article: { author: @article.author, content: @article.content, date: @article.date, title: @article.title } } + assert_redirected_to article_url(@article), 'Article was not successfully updated.' + end + + test "should destroy article" do + assert_difference("Article.count", -1, 'Article destruction failed.') do + delete article_url(@article) + end + + assert_redirected_to articles_url, 'Article was not successfully destroyed.' + end +end diff --git a/test/controllers/pages_controller_test.rb b/test/controllers/pages_controller_test.rb new file mode 100644 index 000000000..eda63beca --- /dev/null +++ b/test/controllers/pages_controller_test.rb @@ -0,0 +1,13 @@ +require "test_helper" + +class PagesControllerTest < ActionDispatch::IntegrationTest + test "should get home" do + get root_url + assert_response :success, 'Failed to get the home page.' + end + + test "should get about" do + get about_url + assert_response :success, 'Failed to get the about page.' + end +end diff --git a/test/models/article_test.rb b/test/models/article_test.rb index 0d55b70ea..42895ee93 100644 --- a/test/models/article_test.rb +++ b/test/models/article_test.rb @@ -2,67 +2,90 @@ class ArticleTest < ActiveSupport::TestCase test 'starts with no articles' do - assert_equal 0, Article.count + assert_equal 0, Article.count, 'Database should start with zero articles.' end test 'has search functionality' do - assert_respond_to Article, :search + assert_respond_to Article, :search, 'Article model should respond to the search method.' end test 'creates a new article' do article = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.') - assert article.valid? + assert article.valid?, "Article creation failed: #{article.errors.full_messages.join(', ')}" + end + + test 'requires a title and content' do + article = Article.new + assert_not article.valid? + assert_includes article.errors[:title], "can't be blank" + assert_includes article.errors[:content], "can't be blank" end test 'displays the article content accurately' do article = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.') - assert_equal 'Lorem ipsum dolor sit amet.', article.content + assert_equal 'Lorem ipsum dolor sit amet.', article.content, "Article content does not match: #{article.errors.full_messages.join(', ')}" end test 'displays the article metadata correctly' do article = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.', author: 'John Doe', date: Date.today) - assert_equal 'John Doe', article.author - assert_equal Date.today, article.date + assert_equal 'John Doe', article.author, "Article author does not match: #{article.errors.full_messages.join(', ')}" + assert_equal Date.today, article.date, "Article date does not match: #{article.errors.full_messages.join(', ')}" end test 'edits an existing article' do article = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.') article.update(content: 'Updated content') - assert_equal 'Updated content', article.content + assert_equal 'Updated content', article.content, "Article content was not updated: #{article.errors.full_messages.join(', ')}" end test 'updates the article metadata' do article = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.', author: 'John Doe', date: Date.today) article.update(author: 'Jane Smith', date: Date.yesterday) - assert_equal 'Jane Smith', article.author - assert_equal Date.yesterday, article.date + assert_equal 'Jane Smith', article.author, "Article author was not updated: #{article.errors.full_messages.join(', ')}" + assert_equal Date.yesterday, article.date, "Article date was not updated: #{article.errors.full_messages.join(', ')}" end test 'deletes an article' do article = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.') article.destroy - assert_equal 0, Article.count + assert_equal 0, Article.count, "Article was not successfully deleted: #{article.errors.full_messages.join(', ')}" end test 'prevents access to deleted articles' do article = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.') article.destroy - assert_raises(ActiveRecord::RecordNotFound) { Article.find(article.id) } + assert_raises(ActiveRecord::RecordNotFound, 'Accessing deleted article should raise ActiveRecord::RecordNotFound') { Article.find(article.id) } end test 'returns accurate search results' do article1 = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.') article2 = Article.create(title: 'Another Article', content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.') results = Article.search('Lorem ipsum') - assert_includes results, article1 - assert_includes results, article2 + assert_includes results, article1, "Search results should include article1: #{article1.errors.full_messages.join(', ')}" + assert_includes results, article2, "Search results should include article2: #{article2.errors.full_messages.join(', ')}" end test 'displays relevant articles in search results' do article1 = Article.create(title: 'Sample Article', content: 'Lorem ipsum dolor sit amet.') article2 = Article.create(title: 'Another Article', content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.') results = Article.search('Another') - assert_includes results, article2 - assert_not_includes results, article1 + assert_includes results, article2, "Search results should include article2 when searching for 'Another': #{article2.errors.full_messages.join(', ')}" + assert_not_includes results, article1, "Search results should not include article1 when searching for 'Another': #{article1.errors.full_messages.join(', ')}" + end + + test 'performs case-insensitive search' do + article = Article.create(title: 'Case Insensitive', content: 'Lorem ipsum dolor sit amet.') + results = Article.search('case insensitive') + assert_includes results, article + end + + test 'returns no results for empty search' do + results = Article.search('') + assert_empty results + end + + test 'returns no results for non-existing term' do + results = Article.search('NonExisting') + assert_empty results end end diff --git a/test/system/articles_test.rb b/test/system/articles_test.rb new file mode 100644 index 000000000..7014628ff --- /dev/null +++ b/test/system/articles_test.rb @@ -0,0 +1,52 @@ +require "application_system_test_case" + +class ArticlesTest < ApplicationSystemTestCase + setup do + @article = Article.create( + title: 'First Post', + content: 'My first article for Shopify', + author: 'Maria Stringy', + date: Date.today + ) + end + + test "visiting the index" do + visit articles_url + assert_selector "h1", text: "Articles" + end + + test "should create article" do + visit articles_url + click_on "New article" + + fill_in "Author", with: @article.author + fill_in "Content", with: @article.content + fill_in "Date", with: @article.date + fill_in "Title", with: @article.title + click_on "Create Article" + + assert_text "Article was successfully created" + click_on "Back" + end + + test "should update Article" do + visit article_url(@article) + click_on "Edit this article", match: :first + + fill_in "Author", with: @article.author + fill_in "Content", with: @article.content + fill_in "Date", with: @article.date + fill_in "Title", with: @article.title + click_on "Update Article" + + assert_text "Article was successfully updated" + click_on "Back" + end + + test "should destroy Article" do + visit article_url(@article) + click_on "Destroy this article", match: :first + + assert_text "Article was successfully destroyed" + end +end