From 96813a5e508a54257ef03be613a704f1f71af53d Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Sat, 21 Oct 2023 00:25:50 -0400 Subject: Added quotes database --- Gemfile | 1 + Gemfile.lock | 5 + app/assets/stylesheets/quotes.css.scss | 4 + app/assets/stylesheets/quotes/layout.css.sass | 215 ++++++++++++++++++++++ app/assets/stylesheets/quotes/pagination.css.scss | 49 +++++ app/controllers/quotes_controller.rb | 97 ++++++++++ app/helpers/quotes_helper.rb | 11 ++ app/models/quote.rb | 33 ++++ app/views/layouts/quotes.html.haml | 31 ++++ app/views/quotes/_quote.html.haml | 16 ++ app/views/quotes/index.atom.builder | 11 ++ app/views/quotes/index.html.haml | 8 + app/views/quotes/list.html.haml | 4 + app/views/quotes/new.html.erb | 2 + app/views/quotes/show.html.haml | 1 + app/views/quotes/tags.html.haml | 4 + app/views/quotes/voted.js.erb | 12 ++ config/initializers/assets.rb | 2 +- config/routes.rb | 14 ++ db/migrate/20231021020306_create_quotes.rb | 14 ++ db/schema.rb | 13 +- test/controllers/quotes_controller_test.rb | 18 ++ test/fixtures/quotes.yml | 17 ++ test/models/quote_test.rb | 7 + 24 files changed, 587 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/quotes.css.scss create mode 100644 app/assets/stylesheets/quotes/layout.css.sass create mode 100644 app/assets/stylesheets/quotes/pagination.css.scss create mode 100644 app/controllers/quotes_controller.rb create mode 100644 app/helpers/quotes_helper.rb create mode 100644 app/models/quote.rb create mode 100644 app/views/layouts/quotes.html.haml create mode 100644 app/views/quotes/_quote.html.haml create mode 100644 app/views/quotes/index.atom.builder create mode 100644 app/views/quotes/index.html.haml create mode 100644 app/views/quotes/list.html.haml create mode 100644 app/views/quotes/new.html.erb create mode 100644 app/views/quotes/show.html.haml create mode 100644 app/views/quotes/tags.html.haml create mode 100644 app/views/quotes/voted.js.erb create mode 100644 db/migrate/20231021020306_create_quotes.rb create mode 100644 test/controllers/quotes_controller_test.rb create mode 100644 test/fixtures/quotes.yml create mode 100644 test/models/quote_test.rb diff --git a/Gemfile b/Gemfile index 887f7eb..fd377fe 100644 --- a/Gemfile +++ b/Gemfile @@ -83,3 +83,4 @@ gem 'akismet' gem 'active_storage_validations' gem "image_processing", ">= 1.2" gem "meta-tags" +gem 'rails_autolink' diff --git a/Gemfile.lock b/Gemfile.lock index 18f75fe..07908ec 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -264,6 +264,10 @@ GEM rails-html-sanitizer (1.6.0) loofah (~> 2.21) nokogiri (~> 1.14) + rails_autolink (1.1.8) + actionview (> 3.1) + activesupport (> 3.1) + railties (> 3.1) railties (7.0.8) actionpack (= 7.0.8) activesupport (= 7.0.8) @@ -382,6 +386,7 @@ DEPENDENCIES paperclip pokeviewer! rails (~> 7.0.3) + rails_autolink redcarpet rouge sassc-rails diff --git a/app/assets/stylesheets/quotes.css.scss b/app/assets/stylesheets/quotes.css.scss new file mode 100644 index 0000000..8cf4eae --- /dev/null +++ b/app/assets/stylesheets/quotes.css.scss @@ -0,0 +1,4 @@ +/* + *= require normalize-rails + *= require_tree ./quotes + */ diff --git a/app/assets/stylesheets/quotes/layout.css.sass b/app/assets/stylesheets/quotes/layout.css.sass new file mode 100644 index 0000000..e5dbd64 --- /dev/null +++ b/app/assets/stylesheets/quotes/layout.css.sass @@ -0,0 +1,215 @@ +body + font-family: "Century Gothic", sans-serif + background-color: rgb(102,102,102) + +#wrap + width: 80% + margin: 1em auto + background-color: white + border: 1px solid black + +#banner + background-color: rgb(153,0,0) + font-size: 1.25em + font-width: bold + padding: 6px + #banner-title + float: left + a + text-decoration: none + color: white + #banner-abbr + color: white + font-style: italic + float: right + +#oneliner + padding: 4px + font-style: italic + background-color: rgb(240,240,240) + a + color: black + cite:before + content: " \2013" + +#top-bar + background-color: black + a + text-decoration: none + &:hover + color: purple + ul + float: right + padding: 4px + li + margin-left: 1em + display: inline + list-style-type: none + a + color: white + font-weight: bold + &.selected a + color: yellow + +.pagination + &+.quote + border-top: 1px solid black + +#quotes + .quote + margin: 0 + padding-bottom: 0.1em + background-color: #fcfcfc + border-bottom: 1px solid black + .quote-header + background-color: #f7f7f7 + border: 1px solid #f4f4f4 + margin: 0 + padding: 0.25em 0.75em + * + margin: 0 0.125em + a + text-decoration: none + .vote-link + color: #888 + .quote-upvote-link + color: #090 + font-weight: bold + .quote-downvote-link + color: #900 + font-weight: bold + .quote-link + font-weight: bold + datetime + font-size: 80% + .quote-edit-link + font-size: 0.9em + float: right + blockquote + font-family: Consolas, Monaco, "Courier New", monospace + font-size: 14px + margin: 0.5em 0.75em + padding: 0 + position: static + width: 100% + &:hover + background-color: #fffcec + .quote-header + background-color: #fec + border-color: #ffe9c9 + .quote-footer + color: #666 + border-top: 1px dashed #EEE + margin: 0 0.75em + padding: 0.25em 0 + * + font-size: 0.9em + .quote-tags + margin: 0 + padding: 0 + list-style-type: none + li + display: inline + a + color: #666 + &:before + content: "TAGS:" + .quote-notes + .quote-tags + margin-top: 0.5em + +#page-body + p.normal + margin: .5em + ul + margin: .5em .5em .5em 2em + h2 + margin: .25em 0 0 .5em + h3 + padding-bottom: 0.25em + border-bottom: 1px dashed gray + margin: .25em 0 0 .5em + .form-field + margin: 0.5em + textarea + width: 100% + #about-left + float: left + width: 48% + margin-right: 1em + #about-right + float: right + width: 48% + margin-left: 1em + margin-bottom: 1em + +#error-messages + margin: .5em + border: 1px solid black + background-color: #FF4040 + padding: .5em + h4 + font-size: 1.25em + +#flash + margin: .5em + border: 1px solid black + background-color: #FBEC5D + padding: .5em + h4 + font-size: 1.25em + +#tags-container + margin: 1em + list-style-type: none + li + display: inline + margin-left: 1em + .css1 + font-size: 1.0em + .css1_5 + font-size: 1.1em + .css2 + font-size: 1.2em + .css2_5 + font-size: 1.3em + .css3 + font-size: 1.4em + .css3_5 + font-size: 1.5em + .css4 + font-size: 1.6em + .css4_5 + font-size: 1.7em + .css5 + font-size: 1.8em + .css5_5 + font-size: 1.9em + .css6 + font-size: 2.0em + +#search-form + margin: 0.5em + input.searchform + width: 50% + +.audioplayer + float: right + +footer + padding: 4px + color: white + text-align: center + font-size: 0.75em + clear: both + font-weight: bold + padding-bottom: .5em + background-color: rgb(153,0,0) + #footer-left + float: left + #footer-right + float: right + a + color: white + +.cleardiv + clear: both \ No newline at end of file diff --git a/app/assets/stylesheets/quotes/pagination.css.scss b/app/assets/stylesheets/quotes/pagination.css.scss new file mode 100644 index 0000000..dad8b88 --- /dev/null +++ b/app/assets/stylesheets/quotes/pagination.css.scss @@ -0,0 +1,49 @@ +.pagination { + text-align: center; + padding: 0.3em; + cursor: default; + margin: 0.5em 0; + + a, span, em { + padding: 0.2em 0.5em; + } + + .disabled { + color: #aaaaaa; + } + + .current { + font-style: normal; + font-weight: bold; + color: #ff0084; + } + + a { + border: 1px solid #dddddd; + color: #0063dc; + text-decoration: none; + + &:hover, &:focus { + border-color: #003366; + background: #0063dc; + color: white; + } + } + + .page_info { + color: #aaaaaa; + padding-top: 0.8em; + } + + .previous_page, .next_page { + border-width: 2px; + } + + .previous_page { + margin-right: 1em; + } + + .next_page { + margin-left: 1em; + } +} \ No newline at end of file diff --git a/app/controllers/quotes_controller.rb b/app/controllers/quotes_controller.rb new file mode 100644 index 0000000..fb5e33c --- /dev/null +++ b/app/controllers/quotes_controller.rb @@ -0,0 +1,97 @@ +class QuotesController < ApplicationController + def index + @quote = Quote.find(310) + @qnumber = Quote.published.count + @mnumber = Quote.pending.count + end + + def latest + @quotes = Quote.published.order(id: :desc).paginate(page: params[:page], per_page: 10) + + respond_to do |format| + format.html { render :list } + format.json { render :json => @quotes } + format.xml { render :xml => @quotes } + format.atom { render :atom => @quotes } + end + end + + def top + @quotes = Quote.published.order(Arel.sql("(upvotes - downvotes) DESC")).paginate(page: params[:page], per_page: 10) + + respond_to do |format| + format.html { render :list } + format.json { render :json => @quotes } + format.xml { render :xml => @quotes } + end + end + + def tags + @tags = Quote.published.tag_counts_on(:tags) + end + + def tag + @quotes = Quote.published.tagged_with(params[:id]).order(id: :desc).paginate(page: params[:page], per_page: 10) + + respond_to do |format| + format.html { render :list } + format.json { render :json => @quotes } + format.xml { render :xml => @quotes } + end + end + + def show + @quote = Quote.published.find(params[:id]) + + respond_to do |format| + format.html + format.json { render :json => @quote } + format.xml { render :xml => @quote } + end + end + + def new + end + + def upvote + @quote = Quote.published.find(params[:id]) + + respond_to do |format| + if @quote.upvote! request.remote_ip + format.html do + flash[:notice] = "You have upvoted Quote \"#{@quote.id}\"." + redirect_to @quote + end + format.js { render "voted" } + format.xml { head :ok } + else + format.html do + flash[:notice] = "You have already voted on Quote \"#{@quote.id}\"." + redirect_to @quote + end + format.xml { render :xml => { :error => "Someone from your IP address has already voted on this quote."} } + end + end + end + + def downvote + @quote = Quote.published.find(params[:id]) + + respond_to do |format| + if @quote.downvote! request.remote_ip + format.html do + flash[:notice] = "You have downvoted Quote \"#{@quote.id}\"." + redirect_to @quote + end + format.js { render "voted" } + format.xml { head :ok } + else + format.html do + flash[:notice] = "You have already voted on Quote \"#{@quote.id}\"." + redirect_to @quote + end + format.xml { render :xml => { :error => "Someone from your IP address has already voted on this quote."} } + end + end + end +end diff --git a/app/helpers/quotes_helper.rb b/app/helpers/quotes_helper.rb new file mode 100644 index 0000000..3c2acf9 --- /dev/null +++ b/app/helpers/quotes_helper.rb @@ -0,0 +1,11 @@ +module QuotesHelper + def quote_format(text) + text = text ? text.to_str : '' + text = text.dup if text.frozen? + text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n + text.gsub!(/\n/, '
') # 1 newline -> br + text = sanitize(text) + text = auto_link(text, :link => :urls) + text + end +end diff --git a/app/models/quote.rb b/app/models/quote.rb new file mode 100644 index 0000000..d037aab --- /dev/null +++ b/app/models/quote.rb @@ -0,0 +1,33 @@ +class Quote < ApplicationRecord + extend Enumerize + + include Votable + + acts_as_taggable + + validates :content, presence: true + + enumerize :state, + in: [:published, :pending, :hidden], + default: :published, + predicates: true + + scope :published, -> { where(state: :published) } + scope :pending, -> { where(state: :pending) } + + def published_date + created_at.strftime("%B %d %Y at %I:%M:%S") + created_at.strftime(" %p").downcase + created_at.strftime(" %Z") + end + + def has_extra? + has_notes? or has_tags? + end + + def has_notes? + !notes.empty? + end + + def has_tags? + !tags.empty? + end +end diff --git a/app/views/layouts/quotes.html.haml b/app/views/layouts/quotes.html.haml new file mode 100644 index 0000000..6d89d91 --- /dev/null +++ b/app/views/layouts/quotes.html.haml @@ -0,0 +1,31 @@ +!!! 5 +%html + %head + %title The Four Island Quotes DB + %meta{ :charset => "utf-8" } + = stylesheet_link_tag "quotes" + = javascript_include_tag "application" + = csrf_meta_tag + = auto_discovery_link_tag :atom, latest_quotes_url(:atom) + %body + #wrap + %header#banner + %h1#banner-title= link_to "The Four Island Quotes DB", root_path + #banner-abbr FIQDB + .cleardiv + %nav#top-bar + %ul + %li= link_to_unless_current "Home", quotes_url + %li= link_to_unless_current "Latest", latest_quotes_url + %li= link_to_unless_current "Top", top_quotes_url + %li= link_to_unless_current "Tags", tags_quotes_url + %li= link_to_unless_current "Feed", latest_quotes_url(:atom) + .cleardiv + #page-body + - if flash[:notice] + #flash= flash[:notice] + = yield + %footer + #footer-left= raw "The Four Island Quotes DB is a #{link_to "Four Island", root_url} project and is © hatkirby 2008-#{Time.now.year}" + #footer-right #{Quote.published.count} approved quotes; #{Quote.pending.count} pending quotes + .cleardiv diff --git a/app/views/quotes/_quote.html.haml b/app/views/quotes/_quote.html.haml new file mode 100644 index 0000000..2a9fb37 --- /dev/null +++ b/app/views/quotes/_quote.html.haml @@ -0,0 +1,16 @@ +%article.quote{ :id => "quote-#{quote.id}" } + %header.quote-header{ :id => "quote-header-#{quote.id}" } + = link_to_unless (quote.new_record? or current_page?(quote)), "\##{quote.id}", quote, :class => "quote-link" + %span.vote-link{ :id => "quote-upvote-link-#{quote.id}" }= link_to_unless (quote.new_record? or quote.already_upvoted?(request.remote_ip)), "Up", upvote_quote_path(quote.id), :remote => true, :rel => "nofollow", :class => "quote-upvote-link", method: :post + %span.quote-rating{ :id => "quote-rating-#{quote.id}" }= "+#{quote.upvotes}/-#{quote.downvotes}" + %span.vote-link{ :id => "quote-downvote-link-#{quote.id}" }= link_to_unless (quote.new_record? or quote.already_downvoted?(request.remote_ip)), "Down", downvote_quote_path(quote.id), :remote => true, :rel => "nofollow", :class => "quote-downvote-link", method: :post + %datetime= quote.published_date + %blockquote.quote-body= raw quote_format(h(quote.content)) + - if !quote.new_record? and quote.has_extra? + .quote-footer + - if quote.has_notes? + .quote-notes= auto_link(quote.notes, :link => :urls) + - if quote.has_tags? + %ul.quote-tags + - quote.tags.each do |tag| + %li= link_to tag.name, tag_quotes_path(tag.name) diff --git a/app/views/quotes/index.atom.builder b/app/views/quotes/index.atom.builder new file mode 100644 index 0000000..66849cc --- /dev/null +++ b/app/views/quotes/index.atom.builder @@ -0,0 +1,11 @@ +atom_feed do |feed| + feed.title("The Four Island Quotes DB") + feed.updated(@quotes.first.created_at) + + @quotes.each do |quote| + feed.entry(quote) do |entry| + entry.title("##{quote.id}") + entry.content(quote.content, :type => 'text') + end + end +end diff --git a/app/views/quotes/index.html.haml b/app/views/quotes/index.html.haml new file mode 100644 index 0000000..d2f667e --- /dev/null +++ b/app/views/quotes/index.html.haml @@ -0,0 +1,8 @@ +#about-left + #quotes= render @quote + %p.normal Welcome to the Four Island Quotes Database! Here you can find many strange and hopefully humorous Four Island quotes. There are currently #{@qnumber} quotes in the database, and there are #{@mnumber} quotes awaiting moderation. +#about-right + %h3 About + %p.normal The Four Island Quotes DB is a repository for humorous and memorable quotes from #{link_to "FourNet", "http://irc.fourisland.com/"} channels, instant messaging sessions, real life situations, and more. + %p.normal The quotes database, in its first incarnation, was created on #{link_to "April 25th 2008", "http://www.fourisland.com/2008/04/quote-time/"} by hatkirby, who was inspired by #{link_to "bash.org", "http://bash.org/"} and his obsession with record-keeping to create a quotes database for Four Island. It ran on the now-defunct PHP quote management system, #{link_to "rash", "http://rqms.sourceforge.net/"}, on the subdomain "quotes.fourisland.com". It was rewritten by hand and integrated into Four Island (at the URL "fourisland.com/quotes") by hatkirby on #{link_to "June 13th 2008", "http://www.fourisland.com/2008/06/the-new-four-island/"} with the release of Four Island 2, dubbed The New Four Island. With the release of Four Island 3 on #{link_to "September 22nd, 2011", "http://www.fourisland.com/2011/09/four-island-3/"}, it was rewritten in Ruby on Rails, disassociated from Four Island and returned to its original URL. +.cleardiv diff --git a/app/views/quotes/list.html.haml b/app/views/quotes/list.html.haml new file mode 100644 index 0000000..12c95c0 --- /dev/null +++ b/app/views/quotes/list.html.haml @@ -0,0 +1,4 @@ +%section#quotes + .pagination= will_paginate @quotes + = render @quotes + .pagination= will_paginate @quotes diff --git a/app/views/quotes/new.html.erb b/app/views/quotes/new.html.erb new file mode 100644 index 0000000..a4c6a0a --- /dev/null +++ b/app/views/quotes/new.html.erb @@ -0,0 +1,2 @@ +

Quotes#new

+

Find me in app/views/quotes/new.html.erb

diff --git a/app/views/quotes/show.html.haml b/app/views/quotes/show.html.haml new file mode 100644 index 0000000..b28c879 --- /dev/null +++ b/app/views/quotes/show.html.haml @@ -0,0 +1 @@ +%section#quotes= render @quote diff --git a/app/views/quotes/tags.html.haml b/app/views/quotes/tags.html.haml new file mode 100644 index 0000000..e15b386 --- /dev/null +++ b/app/views/quotes/tags.html.haml @@ -0,0 +1,4 @@ +%h2 Tag Cloud +%ul#tags-container + - tag_cloud(@tags, %w(css1 css1_5 css2 css2_5 css3 css3_5 css4 css4_5 css5 css5_5 css6)) do |tag, css_class| + %li= link_to tag.name, tag_quotes_path(tag.name), :class => css_class diff --git a/app/views/quotes/voted.js.erb b/app/views/quotes/voted.js.erb new file mode 100644 index 0000000..c697c7d --- /dev/null +++ b/app/views/quotes/voted.js.erb @@ -0,0 +1,12 @@ +$("#quote-rating-<%= @quote.id %>").html('<%= escape_javascript("+#{@quote.upvotes}/-#{@quote.downvotes}") %>'); + +<% if @quote.already_upvoted? request.remote_ip %> + $("#quote-upvote-link-<%= @quote.id %>").html("Up"); +<% elsif @quote.already_downvoted? request.remote_ip %> + $("#quote-downvote-link-<%= @quote.id %>").html("Down"); +<% else %> + $("#quote-upvote-link-<%= @quote.id %>").html('<%= escape_javascript(link_to("Up", upvote_quote_path(@quote.id), :remote => true, :rel => "nofollow", :class => "quote-upvote-link", method: :post)) %>'); + $("#quote-downvote-link-<%= @quote.id %>").html('<%= escape_javascript(link_to("Down", downvote_quote_path(@quote.id), :remote => true, :rel => "nofollow", :class => "quote-downvote-link", method: :post)) %>'); +<% end %> + +$("#quote-header-<%= @quote.id %>").effect('highlight', {}, 2000); diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index e80f11f..ac7deec 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -12,4 +12,4 @@ Rails.application.config.assets.paths << Rails.root.join('node_modules') # 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 ) -Rails.application.config.assets.precompile += %w( main userdata admin ) +Rails.application.config.assets.precompile += %w( main userdata admin quotes ) diff --git a/config/routes.rb b/config/routes.rb index 6363590..33cc5f3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -55,6 +55,20 @@ Rails.application.routes.draw do end end + resources :quotes do + collection do + get 'latest' + get 'top' + get 'tags' + get 'tags/:id', :action => "tag", :as => "tag" + end + + member do + post 'upvote' + post 'downvote' + end + end + mount Pokeviewer::Engine => '/poke3' mount Lingo::Engine => '/lingo' end diff --git a/db/migrate/20231021020306_create_quotes.rb b/db/migrate/20231021020306_create_quotes.rb new file mode 100644 index 0000000..60424e5 --- /dev/null +++ b/db/migrate/20231021020306_create_quotes.rb @@ -0,0 +1,14 @@ +class CreateQuotes < ActiveRecord::Migration[7.0] + def change + create_table :quotes do |t| + t.text :content, null: false + t.string :state, null: false + t.string :submitter + t.text :notes, null: false + t.integer :upvotes, null: false, default: 0 + t.integer :downvotes, null: false, default: 0 + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index f8a8c49..541f1c1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2023_10_20_195330) do +ActiveRecord::Schema[7.0].define(version: 2023_10_21_020306) do create_table "active_storage_attachments", force: :cascade do |t| t.string "name", null: false t.string "record_type", null: false @@ -324,6 +324,17 @@ ActiveRecord::Schema[7.0].define(version: 2023_10_20_195330) do t.index ["world_ribbon_id"], name: "index_pokeviewer_trainers_on_world_ribbon_id" end + create_table "quotes", force: :cascade do |t| + t.text "content", null: false + t.string "state", null: false + t.string "submitter" + t.text "notes", null: false + t.integer "upvotes", default: 0, null: false + t.integer "downvotes", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + create_table "records", force: :cascade do |t| t.text "description" t.string "recordable_type", limit: 191 diff --git a/test/controllers/quotes_controller_test.rb b/test/controllers/quotes_controller_test.rb new file mode 100644 index 0000000..a085d40 --- /dev/null +++ b/test/controllers/quotes_controller_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +class QuotesControllerTest < ActionDispatch::IntegrationTest + test "should get index" do + get quotes_index_url + assert_response :success + end + + test "should get show" do + get quotes_show_url + assert_response :success + end + + test "should get new" do + get quotes_new_url + assert_response :success + end +end diff --git a/test/fixtures/quotes.yml b/test/fixtures/quotes.yml new file mode 100644 index 0000000..761703f --- /dev/null +++ b/test/fixtures/quotes.yml @@ -0,0 +1,17 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + content: MyText + state: MyString + submitter: MyString + notes: MyText + upvotes: 1 + downvotes: 1 + +two: + content: MyText + state: MyString + submitter: MyString + notes: MyText + upvotes: 1 + downvotes: 1 diff --git a/test/models/quote_test.rb b/test/models/quote_test.rb new file mode 100644 index 0000000..d58ff5c --- /dev/null +++ b/test/models/quote_test.rb @@ -0,0 +1,7 @@ +require "test_helper" + +class QuoteTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end -- cgit 1.4.1