summaryrefslogtreecommitdiff
path: root/app/models/concerns
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-20 13:18:24 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-20 13:18:24 +0000
commit0653e08efd039a5905f3fa4f6e9cef9f5d2f799c (patch)
tree4dcc884cf6d81db44adae4aa99f8ec1233a41f55 /app/models/concerns
parent744144d28e3e7fddc117924fef88de5d9674fe4c (diff)
downloadgitlab-ce-0653e08efd039a5905f3fa4f6e9cef9f5d2f799c.tar.gz
Add latest changes from gitlab-org/gitlab@14-3-stable-eev14.3.0-rc42
Diffstat (limited to 'app/models/concerns')
-rw-r--r--app/models/concerns/approvable_base.rb4
-rw-r--r--app/models/concerns/cache_markdown_field.rb33
-rw-r--r--app/models/concerns/calloutable.rb15
-rw-r--r--app/models/concerns/ci/contextable.rb4
-rw-r--r--app/models/concerns/cron_schedulable.rb8
-rw-r--r--app/models/concerns/enums/ci/commit_status.rb1
-rw-r--r--app/models/concerns/featurable.rb21
-rw-r--r--app/models/concerns/has_repository.rb4
-rw-r--r--app/models/concerns/integrations/has_data_fields.rb1
-rw-r--r--app/models/concerns/issuable.rb17
-rw-r--r--app/models/concerns/loose_foreign_key.rb95
-rw-r--r--app/models/concerns/mentionable.rb11
-rw-r--r--app/models/concerns/optimized_issuable_label_filter.rb121
-rw-r--r--app/models/concerns/partitioned_table.rb2
-rw-r--r--app/models/concerns/relative_positioning.rb12
-rw-r--r--app/models/concerns/sanitizable.rb52
-rw-r--r--app/models/concerns/sortable_title.rb21
-rw-r--r--app/models/concerns/taggable_queries.rb10
18 files changed, 235 insertions, 197 deletions
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb
index ef7ba7b1089..8240f9bd6ea 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable_base.rb
@@ -54,4 +54,8 @@ module ApprovableBase
def can_be_approved_by?(user)
user && !approved_by?(user) && user.can?(:approve_merge_request, self)
end
+
+ def can_be_unapproved_by?(user)
+ user && approved_by?(user) && user.can?(:approve_merge_request, self)
+ end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 44d9beff27e..9414d16beef 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -160,39 +160,6 @@ module CacheMarkdownField
# We can only store mentions if the mentionable is a database object
return unless self.is_a?(ApplicationRecord)
- return store_mentions_without_subtransaction! if Feature.enabled?(:store_mentions_without_subtransaction, default_enabled: :yaml)
-
- refs = all_references(self.author)
-
- references = {}
- references[:mentioned_users_ids] = refs.mentioned_user_ids.presence
- references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence
- references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence
-
- # One retry is enough as next time `model_user_mention` should return the existing mention record,
- # that threw the `ActiveRecord::RecordNotUnique` exception in first place.
- self.class.safe_ensure_unique(retries: 1) do
- user_mention = model_user_mention
-
- # this may happen due to notes polymorphism, so noteable_id may point to a record
- # that no longer exists as we cannot have FK on noteable_id
- break if user_mention.blank?
-
- user_mention.mentioned_users_ids = references[:mentioned_users_ids]
- user_mention.mentioned_groups_ids = references[:mentioned_groups_ids]
- user_mention.mentioned_projects_ids = references[:mentioned_projects_ids]
-
- if user_mention.has_mentions?
- user_mention.save!
- else
- user_mention.destroy!
- end
- end
-
- true
- end
-
- def store_mentions_without_subtransaction!
identifier = user_mention_identifier
# this may happen due to notes polymorphism, so noteable_id may point to a record
diff --git a/app/models/concerns/calloutable.rb b/app/models/concerns/calloutable.rb
new file mode 100644
index 00000000000..8b9cfae6a32
--- /dev/null
+++ b/app/models/concerns/calloutable.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Calloutable
+ extend ActiveSupport::Concern
+
+ included do
+ belongs_to :user
+
+ validates :user, presence: true
+ end
+
+ def dismissed_after?(dismissed_after)
+ dismissed_at > dismissed_after
+ end
+end
diff --git a/app/models/concerns/ci/contextable.rb b/app/models/concerns/ci/contextable.rb
index bdba2d3e251..27a704c1de0 100644
--- a/app/models/concerns/ci/contextable.rb
+++ b/app/models/concerns/ci/contextable.rb
@@ -33,13 +33,13 @@ module Ci
#
def simple_variables
strong_memoize(:simple_variables) do
- scoped_variables(environment: nil).to_runner_variables
+ scoped_variables(environment: nil)
end
end
def simple_variables_without_dependencies
strong_memoize(:variables_without_dependencies) do
- scoped_variables(environment: nil, dependencies: false).to_runner_variables
+ scoped_variables(environment: nil, dependencies: false)
end
end
diff --git a/app/models/concerns/cron_schedulable.rb b/app/models/concerns/cron_schedulable.rb
index 48605ecc3d7..d5b86db2640 100644
--- a/app/models/concerns/cron_schedulable.rb
+++ b/app/models/concerns/cron_schedulable.rb
@@ -14,12 +14,10 @@ module CronSchedulable
# The `next_run_at` column is set to the actual execution date of worker that
# triggers the schedule. This way, a schedule like `*/1 * * * *` won't be triggered
# in a short interval when the worker runs irregularly by Sidekiq Memory Killer.
- def calculate_next_run_at
- now = Time.zone.now
+ def calculate_next_run_at(start_time = Time.zone.now)
+ ideal_next_run = ideal_next_run_from(start_time)
- ideal_next_run = ideal_next_run_from(now)
-
- if ideal_next_run == cron_worker_next_run_from(now)
+ if ideal_next_run == cron_worker_next_run_from(start_time)
ideal_next_run
else
cron_worker_next_run_from(ideal_next_run)
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index 16dec5fb081..7f46e44697e 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -26,6 +26,7 @@ module Enums
pipeline_loop_detected: 17,
no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data
trace_size_exceeded: 19,
+ builds_disabled: 20,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
diff --git a/app/models/concerns/featurable.rb b/app/models/concerns/featurable.rb
index ed9bce87da1..70d67fc7559 100644
--- a/app/models/concerns/featurable.rb
+++ b/app/models/concerns/featurable.rb
@@ -83,6 +83,10 @@ module Featurable
end
end
+ included do
+ validate :allowed_access_levels
+ end
+
def access_level(feature)
public_send(self.class.access_level_attribute(feature)) # rubocop:disable GitlabSecurity/PublicSend
end
@@ -94,4 +98,21 @@ module Featurable
def string_access_level(feature)
self.class.str_from_access_level(access_level(feature))
end
+
+ private
+
+ def allowed_access_levels
+ validator = lambda do |field|
+ level = public_send(field) || ENABLED # rubocop:disable GitlabSecurity/PublicSend
+ not_allowed = level > ENABLED
+ self.errors.add(field, "cannot have public visibility level") if not_allowed
+ end
+
+ (self.class.available_features - feature_validation_exclusion).each {|f| validator.call("#{f}_access_level")}
+ end
+
+ # Features that we should exclude from the validation
+ def feature_validation_exclusion
+ []
+ end
end
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 1b4c590694a..9218ba47d20 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -122,4 +122,8 @@ module HasRepository
def after_repository_change_head
reload_default_branch
end
+
+ def after_change_head_branch_does_not_exist(branch)
+ # No-op (by default)
+ end
end
diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb
index e9aaaac8226..1709b56080e 100644
--- a/app/models/concerns/integrations/has_data_fields.rb
+++ b/app/models/concerns/integrations/has_data_fields.rb
@@ -46,6 +46,7 @@ module Integrations
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData'
has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::JiraTrackerData'
has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::OpenProjectTrackerData'
+ has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData'
def data_fields
raise NotImplementedError
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 8d0f8b01d64..5c307158a9a 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -26,6 +26,7 @@ module Issuable
include UpdatedAtFilterable
include ClosedAtFilterable
include VersionedDescription
+ include SortableTitle
TITLE_LENGTH_MAX = 255
TITLE_HTML_LENGTH_MAX = 800
@@ -116,20 +117,6 @@ module Issuable
end
# rubocop:enable GitlabSecurity/SqlInjection
- scope :without_particular_labels, ->(label_names) do
- labels_table = Label.arel_table
- label_links_table = LabelLink.arel_table
- issuables_table = klass.arel_table
- inner_query = label_links_table.project('true')
- .join(labels_table, Arel::Nodes::InnerJoin).on(labels_table[:id].eq(label_links_table[:label_id]))
- .where(label_links_table[:target_type].eq(name)
- .and(label_links_table[:target_id].eq(issuables_table[:id]))
- .and(labels_table[:title].in(label_names)))
- .exists.not
-
- where(inner_query)
- end
-
scope :without_label, -> { joins("LEFT OUTER JOIN label_links ON label_links.target_type = '#{name}' AND label_links.target_id = #{table_name}.id").where(label_links: { id: nil }) }
scope :with_label_ids, ->(label_ids) { joins(:label_links).where(label_links: { label_id: label_ids }) }
scope :join_project, -> { joins(:project) }
@@ -293,6 +280,8 @@ module Issuable
when 'popularity', 'popularity_desc', 'upvotes_desc' then order_upvotes_desc
when 'priority', 'priority_asc' then order_due_date_and_labels_priority(excluded_labels: excluded_labels)
when 'priority_desc' then order_due_date_and_labels_priority('DESC', excluded_labels: excluded_labels)
+ when 'title_asc' then order_title_asc.with_order_id_desc
+ when 'title_desc' then order_title_desc.with_order_id_desc
else order_by(method)
end
diff --git a/app/models/concerns/loose_foreign_key.rb b/app/models/concerns/loose_foreign_key.rb
new file mode 100644
index 00000000000..4e822a04869
--- /dev/null
+++ b/app/models/concerns/loose_foreign_key.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module LooseForeignKey
+ extend ActiveSupport::Concern
+
+ # This concern adds loose foreign key support to ActiveRecord models.
+ # Loose foreign keys allow delayed processing of associated database records
+ # with similar guarantees than a database foreign key.
+ #
+ # TODO: finalize this later once the async job is in place
+ #
+ # Prerequisites:
+ #
+ # To start using the concern, you'll need to install a database trigger to the parent
+ # table in a standard DB migration (not post-migration).
+ #
+ # > add_loose_foreign_key_support(:projects, :gitlab_main)
+ #
+ # Usage:
+ #
+ # > class Ci::Build < ApplicationRecord
+ # >
+ # > loose_foreign_key :security_scans, :build_id, on_delete: :async_delete, gitlab_schema: :gitlab_main
+ # >
+ # > # associations can be still defined, the dependent options is no longer necessary:
+ # > has_many :security_scans, class_name: 'Security::Scan'
+ # >
+ # > end
+ #
+ # Options for on_delete:
+ #
+ # - :async_delete - deletes the children rows via an asynchronous process.
+ # - :async_nullify - sets the foreign key column to null via an asynchronous process.
+ #
+ # Options for gitlab_schema:
+ #
+ # - :gitlab_ci
+ # - :gitlab_main
+ #
+ # The value can be determined by calling `Model.gitlab_schema` where the Model represents
+ # the model for the child table.
+ #
+ # How it works:
+ #
+ # When adding loose foreign key support to the table, a DELETE trigger is installed
+ # which tracks the record deletions (stores primary key value of the deleted row) in
+ # a database table.
+ #
+ # These deletion records are processed asynchronously and records are cleaned up
+ # according to the loose foreign key definitions described in the model.
+ #
+ # The cleanup happens in batches, which reduces the likelyhood of statement timeouts.
+ #
+ # When all associations related to the deleted record are cleaned up, the record itself
+ # is deleted.
+ included do
+ class_attribute :loose_foreign_key_definitions, default: []
+ end
+
+ class_methods do
+ def loose_foreign_key(to_table, column, options)
+ symbolized_options = options.symbolize_keys
+
+ unless base_class?
+ raise <<~MSG
+ loose_foreign_key can be only used on base classes, inherited classes are not supported.
+ Please define the loose_foreign_key on the #{base_class.name} class.
+ MSG
+ end
+
+ on_delete_options = %i[async_delete async_nullify]
+ gitlab_schema_options = [ApplicationRecord.gitlab_schema, Ci::ApplicationRecord.gitlab_schema]
+
+ unless on_delete_options.include?(symbolized_options[:on_delete]&.to_sym)
+ raise "Invalid on_delete option given: #{symbolized_options[:on_delete]}. Valid options: #{on_delete_options.join(', ')}"
+ end
+
+ unless gitlab_schema_options.include?(symbolized_options[:gitlab_schema]&.to_sym)
+ raise "Invalid gitlab_schema option given: #{symbolized_options[:gitlab_schema]}. Valid options: #{gitlab_schema_options.join(', ')}"
+ end
+
+ definition = ActiveRecord::ConnectionAdapters::ForeignKeyDefinition.new(
+ table_name.to_s,
+ to_table.to_s,
+ {
+ column: column.to_s,
+ on_delete: symbolized_options[:on_delete].to_sym,
+ gitlab_schema: symbolized_options[:gitlab_schema].to_sym
+ }
+ )
+
+ self.loose_foreign_key_definitions += [definition]
+ end
+ end
+end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 4df9e32d8ec..a0ea5ac8012 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -217,17 +217,6 @@ module Mentionable
def user_mention_association
association(:user_mentions).reflection
end
-
- # User mention that is parsed from model description rather then its related notes.
- # Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
- # Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
- # a description attribute.
- #
- # Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
- # in a multi-threaded environment. Make sure to use it within a *safe_ensure_unique* block.
- def model_user_mention
- user_mentions.where(note_id: nil).first_or_initialize
- end
end
Mentionable.prepend_mod_with('Mentionable')
diff --git a/app/models/concerns/optimized_issuable_label_filter.rb b/app/models/concerns/optimized_issuable_label_filter.rb
deleted file mode 100644
index 19d2ac620f3..00000000000
--- a/app/models/concerns/optimized_issuable_label_filter.rb
+++ /dev/null
@@ -1,121 +0,0 @@
-# frozen_string_literal: true
-
-module OptimizedIssuableLabelFilter
- extend ActiveSupport::Concern
-
- prepended do
- extend Gitlab::Cache::RequestCache
-
- # Avoid repeating label queries times when the finder is instantiated multiple times during the request.
- request_cache(:find_label_ids) { [root_namespace.id, params.label_names] }
- end
-
- def by_label(items)
- return items unless params.labels?
-
- return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
-
- target_model = items.model
-
- if params.filter_by_no_label?
- items.where('NOT EXISTS (?)', optimized_any_label_query(target_model))
- elsif params.filter_by_any_label?
- items.where('EXISTS (?)', optimized_any_label_query(target_model))
- else
- issuables_with_selected_labels(items, target_model)
- end
- end
-
- # Taken from IssuableFinder
- def count_by_state
- return super if Feature.disabled?(:optimized_issuable_label_filter, default_enabled: :yaml)
-
- count_params = params.merge(state: nil, sort: nil, force_cte: true)
- finder = self.class.new(current_user, count_params)
-
- state_counts = finder
- .execute
- .reorder(nil)
- .group(:state_id)
- .count
-
- counts = Hash.new(0)
-
- state_counts.each do |key, value|
- counts[count_key(key)] += value
- end
-
- counts[:all] = counts.values.sum
- counts.with_indifferent_access
- end
-
- private
-
- def issuables_with_selected_labels(items, target_model)
- if root_namespace
- all_label_ids = find_label_ids
- # Found less labels in the DB than we were searching for. Return nothing.
- return items.none if all_label_ids.size != params.label_names.size
-
- all_label_ids.each do |label_ids|
- items = items.where('EXISTS (?)', optimized_label_query_by_label_ids(target_model, label_ids))
- end
- else
- params.label_names.each do |label_name|
- items = items.where('EXISTS (?)', optimized_label_query_by_label_name(target_model, label_name))
- end
- end
-
- items
- end
-
- def find_label_ids
- group_labels = Label
- .where(project_id: nil)
- .where(title: params.label_names)
- .where(group_id: root_namespace.self_and_descendants.select(:id))
-
- project_labels = Label
- .where(group_id: nil)
- .where(title: params.label_names)
- .where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendants.select(:id)))
-
- Label
- .from_union([group_labels, project_labels], remove_duplicates: false)
- .reorder(nil)
- .pluck(:title, :id)
- .group_by(&:first)
- .values
- .map { |labels| labels.map(&:last) }
- end
-
- def root_namespace
- strong_memoize(:root_namespace) do
- (params.project || params.group)&.root_ancestor
- end
- end
-
- def optimized_any_label_query(target_model)
- LabelLink
- .where(target_type: target_model.name)
- .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
- .limit(1)
- end
-
- def optimized_label_query_by_label_ids(target_model, label_ids)
- LabelLink
- .where(target_type: target_model.name)
- .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
- .where(label_id: label_ids)
- .limit(1)
- end
-
- def optimized_label_query_by_label_name(target_model, label_name)
- LabelLink
- .joins(:label)
- .where(target_type: target_model.name)
- .where(LabelLink.arel_table['target_id'].eq(target_model.arel_table['id']))
- .where(labels: { name: label_name })
- .limit(1)
- end
-end
diff --git a/app/models/concerns/partitioned_table.rb b/app/models/concerns/partitioned_table.rb
index eab5d4c35bb..23d2d00b346 100644
--- a/app/models/concerns/partitioned_table.rb
+++ b/app/models/concerns/partitioned_table.rb
@@ -14,8 +14,6 @@ module PartitionedTable
strategy_class = PARTITIONING_STRATEGIES[strategy.to_sym] || raise(ArgumentError, "Unknown partitioning strategy: #{strategy}")
@partitioning_strategy = strategy_class.new(self, partitioning_key, **kwargs)
-
- Gitlab::Database::Partitioning::PartitionManager.register(self)
end
end
end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 75dfed6d58f..c32e499c329 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -135,21 +135,21 @@ module RelativePositioning
before, after = [before, after].sort_by(&:relative_position) if before && after
RelativePositioning.mover.move(self, before, after)
- rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ rescue NoSpaceLeft => e
could_not_move(e)
raise e
end
def move_after(before = self)
RelativePositioning.mover.move(self, before, nil)
- rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ rescue NoSpaceLeft => e
could_not_move(e)
raise e
end
def move_before(after = self)
RelativePositioning.mover.move(self, nil, after)
- rescue ActiveRecord::QueryCanceled, NoSpaceLeft => e
+ rescue NoSpaceLeft => e
could_not_move(e)
raise e
end
@@ -159,9 +159,6 @@ module RelativePositioning
rescue NoSpaceLeft => e
could_not_move(e)
self.relative_position = MAX_POSITION
- rescue ActiveRecord::QueryCanceled => e
- could_not_move(e)
- raise e
end
def move_to_start
@@ -169,9 +166,6 @@ module RelativePositioning
rescue NoSpaceLeft => e
could_not_move(e)
self.relative_position = MIN_POSITION
- rescue ActiveRecord::QueryCanceled => e
- could_not_move(e)
- raise e
end
# This method is used during rebalancing - override it to customise the update
diff --git a/app/models/concerns/sanitizable.rb b/app/models/concerns/sanitizable.rb
new file mode 100644
index 00000000000..05756beb404
--- /dev/null
+++ b/app/models/concerns/sanitizable.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+# == Sanitizable concern
+#
+# This concern adds HTML sanitization and validation to models. The intention is
+# to help prevent XSS attacks in the event of a by-pass in the frontend
+# sanitizer due to a configuration issue or a vulnerability in the sanitizer.
+# This approach is commonly referred to as defense-in-depth.
+#
+# Example:
+#
+# module Dast
+# class Profile < ApplicationRecord
+# include Sanitizable
+#
+# sanitizes! :name, :description
+
+module Sanitizable
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def sanitize(input)
+ return unless input
+
+ # We return the input unchanged to avoid escaping pre-escaped HTML fragments.
+ # Please see gitlab-org/gitlab#293634 for an example.
+ return input unless input == CGI.unescapeHTML(input.to_s)
+
+ CGI.unescapeHTML(Sanitize.fragment(input))
+ end
+
+ def sanitizes!(*attrs)
+ instance_eval do
+ before_validation do
+ attrs.each do |attr|
+ input = public_send(attr) # rubocop: disable GitlabSecurity/PublicSend
+
+ public_send("#{attr}=", self.class.sanitize(input)) # rubocop: disable GitlabSecurity/PublicSend
+ end
+ end
+
+ validates_each(*attrs) do |record, attr, input|
+ # We reject pre-escaped HTML fragments as invalid to avoid saving them
+ # to the database.
+ unless input.to_s == CGI.unescapeHTML(input.to_s)
+ record.errors.add(attr, 'cannot contain escaped HTML entities')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/sortable_title.rb b/app/models/concerns/sortable_title.rb
new file mode 100644
index 00000000000..7c5cad17f4c
--- /dev/null
+++ b/app/models/concerns/sortable_title.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module SortableTitle
+ extend ActiveSupport::Concern
+
+ included do
+ scope :order_title_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:title].lower)) }
+ scope :order_title_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:title].lower)) }
+ end
+
+ class_methods do
+ def simple_sorts
+ super.merge(
+ {
+ 'title_asc' => -> { order_title_asc },
+ 'title_desc' => -> { order_title_desc }
+ }
+ )
+ end
+ end
+end
diff --git a/app/models/concerns/taggable_queries.rb b/app/models/concerns/taggable_queries.rb
index cba2e93a86d..06799f0a9f4 100644
--- a/app/models/concerns/taggable_queries.rb
+++ b/app/models/concerns/taggable_queries.rb
@@ -3,6 +3,10 @@
module TaggableQueries
extend ActiveSupport::Concern
+ MAX_TAGS_IDS = 50
+
+ TooManyTagsError = Class.new(StandardError)
+
class_methods do
# context is a name `acts_as_taggable context`
def arel_tag_names_array(context = :tags)
@@ -34,4 +38,10 @@ module TaggableQueries
where("EXISTS (?)", matcher)
end
end
+
+ def tags_ids
+ tags.limit(MAX_TAGS_IDS).order('id ASC').pluck(:id).tap do |ids|
+ raise TooManyTagsError if ids.size >= MAX_TAGS_IDS
+ end
+ end
end