aboutsummaryrefslogtreecommitdiff
path: root/app/models
diff options
context:
space:
mode:
Diffstat (limited to 'app/models')
-rw-r--r--app/models/application_record.rb5
-rw-r--r--app/models/comment.rb18
-rw-r--r--app/models/concerns/.keep0
-rw-r--r--app/models/search.rb99
-rw-r--r--app/models/tag.rb7
-rw-r--r--app/models/tag_reference.rb6
-rw-r--r--app/models/thing.rb32
-rw-r--r--app/models/user.rb9
8 files changed, 176 insertions, 0 deletions
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/comment.rb b/app/models/comment.rb
new file mode 100644
index 0000000..deb6ad7
--- /dev/null
+++ b/app/models/comment.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class Comment < ApplicationRecord
+ validates :content, presence: true
+
+ belongs_to :thing
+
+ has_many :tag_references, as: :taggable, dependent: :destroy
+ has_many :tags, through: :tag_references
+
+ has_rich_text :content
+
+ # Returns all comments which have a content matching the given text.
+ def self.like(text:)
+ Comment.joins(:rich_text_content)
+ .where('action_text_rich_texts.body LIKE ?', "%#{text}%")
+ end
+end
diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/app/models/concerns/.keep
diff --git a/app/models/search.rb b/app/models/search.rb
new file mode 100644
index 0000000..81a1c34
--- /dev/null
+++ b/app/models/search.rb
@@ -0,0 +1,99 @@
+# frozen_string_literal: true
+
+class Search < ApplicationRecord
+ validates :name, presence: true, uniqueness: true
+ validates :body, presence: true, uniqueness: true
+
+ belongs_to :user
+
+ # Returns all the results that can be fetched with the current `body`. It's
+ # returned into a hash which groups the taggable types.
+ def results
+ # This can happen as part of an empty Search.new instance. That is, it's
+ # still invalid, but it's the object being initialized for the default
+ # search (i.e. just show me everything).
+ return { things: Thing.order('rate DESC, created_at'), comments: [] } if body.blank?
+
+ # Parse the body of the search, and return early if nothing was able to be
+ # parsed (e.g. user wrote unknown identifiers).
+ parsed = parse_body
+ return { things: [], comments: [] } if parsed.all? { |_, v| v.blank? }
+
+ # And add everything into the `res` hash with the results.
+ res = find_by_text(plain: parsed[:plain])
+ find_by_tags(res:, tags: parsed[:tag])
+ end
+
+ protected
+
+ # Returns a hash which contains the tags that has been specified (`tag`) and
+ # the plain text (`plain`) to check on the different fields.
+ def parse_body
+ res = { tag: [], plain: [] }
+
+ body.split.each do |part|
+ if part.include?(':')
+ parts = part.split(':', 2)
+ parts[1] = clean_clause(part: parts[1])
+
+ next if parts[0] != 'tag'
+
+ res[parts[0].to_sym] << parts[1]
+ else
+ res[:plain] << part
+ end
+ end
+
+ res
+ end
+
+ # Returns the argument without any leading/trailing quotes.
+ def clean_clause(part:)
+ part.gsub(/^("|')+/, '').gsub(/("|')+$/, '')
+ end
+
+ # Returns a hash which groups into taggable types the results by looking the
+ # `plain` text into different fields.
+ def find_by_text(plain:)
+ res = { things: [], comments: [] }
+
+ plain.each do |text|
+ res[:things] = if res[:things].any?
+ res[:things].and(Thing.like(text:)).order('rate DESC, created_at')
+ else
+ Thing.like(text:).order('rate DESC, created_at')
+ end
+ res[:comments] = if res[:comments].any?
+ res[:comments].and(Comment.like(text:)).order(:created_at)
+ else
+ Comment.like(text:).order(:created_at)
+ end
+ end
+
+ res
+ end
+
+ # Returns a hash which groups into taggable types the results by matching the
+ # given `tags` which their references. It expects `res` to be already
+ # initialized by `find_by_text` (yeah, great design, I know), which is also
+ # the structure that will be returned.
+ def find_by_tags(res:, tags:)
+ return res if tags.blank?
+
+ tags = tags.map { |name| Tag.find_by(name:) }
+
+ query = TagReference.where(tag: tags, taggable_type: 'Thing')
+ query = query.where(taggable_id: res[:things].pluck(:id)) if res[:things].present?
+ res[:things] = query.group(:taggable_id)
+ .having('count(taggable_id) = ?', tags.size)
+ .map(&:taggable)
+
+ query = TagReference.where(tag: tags, taggable_type: 'Comment')
+ query = query.where(taggable_id: res[:comments].pluck(:id)) if res[:comments].present?
+ res[:comments] = query.group(:taggable_id)
+ .having('count(taggable_id) = ?', tags.size)
+ .map(&:taggable)
+
+ res
+ end
+end
diff --git a/app/models/tag.rb b/app/models/tag.rb
new file mode 100644
index 0000000..1f359d6
--- /dev/null
+++ b/app/models/tag.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class Tag < ApplicationRecord
+ validates :name, presence: true, uniqueness: true
+
+ has_many :tag_references, dependent: :destroy
+end
diff --git a/app/models/tag_reference.rb b/app/models/tag_reference.rb
new file mode 100644
index 0000000..bd72267
--- /dev/null
+++ b/app/models/tag_reference.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class TagReference < ApplicationRecord
+ belongs_to :tag
+ belongs_to :taggable, polymorphic: true
+end
diff --git a/app/models/thing.rb b/app/models/thing.rb
new file mode 100644
index 0000000..5c758fb
--- /dev/null
+++ b/app/models/thing.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class Thing < ApplicationRecord
+ validates :title, presence: true, uniqueness: true
+ validates :target, presence: true, uniqueness: true
+ validates :authors, presence: true
+ validates :rate, numericality: { in: 0..10 }
+
+ belongs_to :user
+
+ has_many :comments, dependent: :destroy
+ has_many :tag_references, as: :taggable, dependent: :destroy
+ has_many :tags, through: :tag_references
+
+ enum :status, %i[read notread tobepublished], validate: true
+ enum :kind, %i[other poetry theater essay shorts novel paper], validate: true
+
+ # Returns all things which match the given text for any of the string columns
+ # from the table.
+ def self.like(text:)
+ Thing.where('target LIKE ?', "%#{text}%")
+ .or(where('title LIKE ?', "%#{text}%"))
+ .or(where('publisher LIKE ?', "%#{text}%"))
+ .or(where('address LIKE ?', "%#{text}%"))
+ .or(where('url LIKE ?', "%#{text}%"))
+ .or(where('location LIKE ?', "%#{text}%"))
+ .or(where('insideof LIKE ?', "%#{text}%"))
+ .or(where('pages LIKE ?', "%#{text}%"))
+ .or(where('note LIKE ?', "%#{text}%"))
+ .or(where('authors LIKE ?', "%#{text}%"))
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
new file mode 100644
index 0000000..7eaba55
--- /dev/null
+++ b/app/models/user.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class User < ApplicationRecord
+ has_secure_password
+
+ validates :username, presence: true, uniqueness: true
+ validates :password, presence: true, on: :create
+ validates :password_confirmation, presence: true, on: :create
+end