app/models/description.rb
# frozen_string_literal: true
#
# = Base Class for Models with Authored Descriptions
#
# This class provides the common functionality between NameDescription and
# LocationDescription.
#
# == Class Methods
#
# None.
#
# == Instance Methods
#
# ==== Title formats
# text_name:: Description of Agaricus from Source
# unique_text_name:: Description of Agaricus from Source (123)
# format_name:: Description of **__Agaricus__** from __Source__
# unique_format_name:: Description of **__Agaricus__** from __Source__ (123)
# partial_text_name:: Description from Source
# unique_partial_text_name:: Description from Source (123)
# partial_format_name:: Description from __Source__
# unique_partial_format_name:: Description from __Source__ (123)
#
# ==== Past Versions
# versions:: List of past versions.
# versioned_table_name:: Table used to keep past versions.
#
# ==== Descriptive Text
# notes?:: Are any of the notes fields non-empty?
# all_notes:: Return all the notes fields via a Hash.
# all_notes=:: Change all the notes fields via a Hash.
# note_status:: Return some basic stats on notes fields.
#
# ==== Source Info
# source_type:: Category of source, e.g. "public", "project", "user".
# source_name:: Source identifier (e.g., Project title).
# source_object:: Return reference to object representing source.
# belongs_to_project?:: Does this Description belong to a given Project?
#
# ==== Permissions
# admins:: User's with admin privileges.
# writers:: User's with write privileges.
# readers:: User's with read privileges.
# admin_ids:: User's with admin privileges, as Array of ids.
# writer_ids:: User's with write privileges, as Array of ids.
# reader_ids:: User's with read privileges, as Array of ids.
# is_admin?:: Does a given User have admin privileges?
# writer?:: Does a given User have write privileges?
# is_reader?:: Does a given User have read privileges?
# add_admin:: Give a User or UserGroup admin privileges.
# add_writer:: Give a User or UserGroup writer privileges.
# add_reader:: Give a User or UserGroup reader privileges.
# remove_admin:: Remove a User's or UserGroup's admin privileges.
# remove_writer:: Remove a User's or UserGroup's writer privileges.
# remove_reader:: Remove a User's or UserGroup's reader privileges.
# permitted?:: Does a given User have a given type of permission?
# group_user_ids:: List of user ids from a given permissions table.
# group_ids:: List of user_group ids from a given permissions table.
# admins_join_table:: Table used to list admin groups.
# writers_join_table:: Table used to list writer groups.
# readers_join_table:: Table used to list reader groups.
# public:: Attribute that is +true+ if all users can read.
# public_write:: Fake attribute that is +true+ if all users can write.
#
# ==== Authors and Editors
# editors:: User's that have edited this Name.
# authors:: User's that have made "significant" contributions.
# editor?:: Is a given User an editor?
# author?:: Is a given User an author?
# add_editor:: Make given user an "editor".
# add_author:: Make given user an "author".
# remove_author:: Demote given user to "editor".
# authors_join_table:: Table used to list authors.
# editors_join_table:: Table used to list editors.
#
# == Callbacks
# before_save:: Add User as author/editor before making change.
# before_destroy:: Subtract authorship/editorship contributions
# before destroy.
#
#
class Description < AbstractModel
self.abstract_class = true
before_save :add_author_or_editor
before_destroy :update_users_and_parent
# Aliases for location / name.
def parent
send(parent_type)
end
def parent_id
send(:"#{parent_type}_id")
end
def parent=(val)
send(:"#{parent_type}=", val)
end
def parent_id=(val)
send(:"#{parent_type}_id=", val)
end
# Return parent's class name in lowercase, e.g. 'name' or 'location'.
def parent_type
type_tag.to_s.sub("_description", "")
end
# Shorthand for "public && public_write"
def fully_public?
public && public_write
end
# Is this group writable by the general public?
def public_write
@public_write ||= public_write_was
end
# Change state of +public_write+.
attr_writer :public_write
# Get the initial state of +public_write+ before modification by form.
def public_write_was
writer_group_ids == [UserGroup.all_users.id]
end
##############################################################################
#
# :section: Title/Name Formats
#
##############################################################################
# Descriptive title including parent name, in plain text.
def text_name
put_together_name(:full).t.html_to_ascii
end
# Same as +text_name+ but with id tacked on.
def unique_text_name
string_with_id(text_name)
end
# Descriptive title including parent name, in Textile-formatted text.
def format_name
put_together_name(:full)
end
# Same as +format_name+ but with id tacked on.
def unique_format_name
string_with_id(format_name)
end
# Descriptive title without parent name, in plain text.
def partial_text_name
put_together_name(:part).t.html_to_ascii
end
# Same as +partial_text_name+ but with id tacked on.
def unique_partial_text_name
string_with_id(partial_text_name)
end
# Descriptive title without parent name, in Textile-formatted text.
def partial_format_name
put_together_name(:part)
end
# Same as +partial_format_name+ but with id tacked on.
def unique_partial_format_name
string_with_id(partial_format_name)
end
##############################################################################
#
# :section: Descriptions
#
##############################################################################
# Are any of the descriptive text fields non-empty?
def notes?
result = false
self.class.all_note_fields.each do |field|
result = send(field).to_s.match(/\S/)
break if result
end
result
end
# Returns a Hash containing all the descriptive text fields. (See also the
# counterpart writer-method +all_notes=+.)
def all_notes
result = {}
self.class.all_note_fields.each do |field|
value = send(field).to_s
result[field] = value.presence
end
result
end
# Update all the descriptive text fields via Hash.
#
# hash = name.all_notes
# hash[:look_alikes] = "new value"
# name.all_notes = hash
#
def all_notes=(notes)
self.class.all_note_fields.each do |field|
send(:"#{field}=", notes[field])
end
end
# Find out how much descriptive text has been written for this object.
# Returns the number of fields filled in, and how many characters total.
#
# num_fields, total_length = name.note_status
#
def note_status
field_count = size_count = 0
all_notes.each_value do |v|
if v.present?
field_count += 1
size_count += v.strip_squeeze.length
end
end
[field_count, size_count]
end
##############################################################################
#
# :section: Sources
#
##############################################################################
ALL_SOURCE_TYPES = [
"public", # Public ones created by any user.
"foreign", # Foreign "public" description(s) written on another server.
"source", # Derived from another source, e.g. another website or book.
"project", # Draft created for a project.
"user" # Created by an individual user.
].freeze
BASIC_SOURCE_TYPES = [
"public", # Public ones created by any user.
"source", # Derived from another source, e.g. another website or book.
"user" # Created by an individual user.
].freeze
# Return an Array of source type Strings, e.g. "public", "project", etc.
# Note, this is the order they will be listed in show_name descriptions
# panel, list_descriptions.
def self.all_source_types
ALL_SOURCE_TYPES
# NOTE: Why not keep this simple and just load them in order of the enums?
# source_types.map do |name, _integer|
# name
# end
end
def self.basic_source_types
BASIC_SOURCE_TYPES
end
# Retreive object representing the source (if applicable). Presently, this
# only works for Project drafts and User's personal descriptions. All others
# return +nil+.
def source_object
case source_type
# (this may eventually be replaced with source_id)
when "project" then project
when "source" then nil # (haven't created "Source" model yet)
when "user" then user
end
end
# Does this Description belong to a given Project?
def belongs_to_project?(project)
(source_type == "project") &&
(project_id == project.id)
end
##############################################################################
#
# :section: Permissions
#
##############################################################################
# Name of the join table used to keep admin groups.
def self.admins_join_table
:"#{table_name.singularize}_admins"
end
# Wrapper around class method of same name
def admins_join_table
self.class.admins_join_table
end
# Name of the join table used to keep writer groups.
def self.writers_join_table
:"#{table_name.singularize}_writers"
end
# Wrapper around class method of same name
def writers_join_table
self.class.writers_join_table
end
# Name of the join table used to keep reader groups.
def self.readers_join_table
:"#{table_name.singularize}_readers"
end
# Wrapper around class method of same name
def readers_join_table
self.class.readers_join_table
end
# List of all the admins for this description
def admins
group_users(admins_join_table)
end
# List of all the writers for this description
def writers
group_users(writers_join_table)
end
# List of all the readers for this description
def readers
group_users(readers_join_table)
end
# List of all the admins for this description, as ids.
def admin_ids
group_user_ids(admins_join_table)
end
# List of all the writers for this description, as ids.
def writer_ids
group_user_ids(writers_join_table)
end
# List of all the readers for this description, as ids.
def reader_ids
group_user_ids(readers_join_table)
end
# Is a given user an admin for this description?
def is_admin?(user)
permitted?(admins_join_table, user)
end
# Is a given user an writer for this description?
def writer?(user)
public_write || permitted?(writers_join_table, user)
end
# Is a given user an reader for this description?
def is_reader?(user)
public || permitted?(readers_join_table, user)
end
# Give a User or UserGroup admin privileges.
def add_admin(arg)
chg_permission(admins, arg, :add)
end
# Give a User or UserGroup writer privileges.
def add_writer(arg)
chg_permission(writers, arg, :add)
end
# Give a User or UserGroup reader privileges.
def add_reader(arg)
chg_permission(readers, arg, :add)
end
# Revoke a User's or UserGroup's admin privileges.
def remove_admin(arg)
chg_permission(admins, arg, :remove)
end
# Revoke a User's or UserGroup's writer privileges.
def remove_writer(arg)
chg_permission(writers, arg, :remove)
end
# Revoke a User's or UserGroup's reader privileges.
def remove_reader(arg)
chg_permission(readers, arg, :remove)
end
# Check if a given user has the given type of permission.
def permitted?(table, user)
if user.is_a?(User)
group_user_ids(table).include?(user.id)
elsif !user
group_ids(table).include?(UserGroup.all_users.id)
elsif user.try(:to_i)&.nonzero?
group_user_ids(table).include?(user.to_i)
else
raise(ArgumentError.new("Was expecting User instance, id or nil."))
end
end
# Do minimal query to enumerate the users in a list of groups. Return as an
# Array of User instances. Caches result.
def group_users(table)
@group_users ||= {}
@group_users[table] ||= User.where(id: group_user_ids(table)).to_a
end
private
# Do minimal query to enumerate the users in a list of groups. Return as an
# Array of ids. Caches result.
def group_user_ids(table)
@group_user_ids ||= {}
@group_user_ids[table] ||=
table.to_s.classify.constantize.
joins(user_group: :user_group_users).
where("#{type_tag}_id" => id).
order(user_id: :asc).distinct.
pluck(:user_id)
end
# Do minimal query to enumerate a list of groups. Return as an Array of ids.
# Caches result. (Equivalent to using <tt>association.ids</tt>, I think.)
def group_ids(table)
@group_ids ||= {}
@group_ids[table] ||=
table.to_s.classify.constantize.
where("#{type_tag}_id" => id).
order(user_group_id: :asc).distinct.
pluck(:user_group_id)
end
public
##############################################################################
#
# :section: Authors and Editors
#
##############################################################################
# Name of the join table used to keep authors.
def self.authors_join_table
:"#{table_name.singularize}_authors"
end
# Wrapper around class method of same name
def authors_join_table
self.class.authors_join_table
end
# Name of the join table used to keep editors.
def self.editors_join_table
:"#{table_name.singularize}_editors"
end
# Wrapper around class method of same name
def editors_join_table
self.class.editors_join_table
end
# Is the given User an author?
def author?(user)
authors.member?(user)
end
# Is the given User an editor?
def editor?(user)
editors.member?(user)
end
# Add a User on as an "author". Saves User if changed. Returns nothing.
def add_author(user)
return if authors.member?(user)
authors.push(user)
UserStats.update_contribution(:add, authors_join_table, user.id)
return unless editors.member?(user)
editors.delete(user)
UserStats.update_contribution(:del, editors_join_table, user.id)
end
# Demote a User to "editor". Saves User if changed. Returns nothing.
def remove_author(user)
return unless authors.member?(user)
authors.delete(user)
UserStats.update_contribution(:del, authors_join_table, user.id)
return unless !editors.member?(user) && user_made_a_change?(user)
editors.push(user)
UserStats.update_contribution(:add, editors_join_table, user.id)
end
# Add a user on as an "editor".
def add_editor(user)
return unless !authors.member?(user) && !editors.member?(user)
editors.push(user)
UserStats.update_contribution(:add, editors_join_table, user.id)
end
##############################################################################
#
# :section: Callbacks
#
##############################################################################
# Callback that updates editors and/or authors after a User makes a change.
# If the Name has no author and they've made sufficient contributions, they
# get promoted to author by default. In all cases make sure the user is
# added on as an editor.
def add_author_or_editor
return unless !@save_without_our_callbacks && (user = User.current)
authors.empty? && author_worthy? ? add_author(user) : add_editor(user)
end
# When destroying an object, subtract contributions due to
# authorship/editorship.
def update_users_and_parent
# Update editors' and authors' contributions.
authors.each do |user|
UserStats.update_contribution(:del, authors_join_table, user.id)
end
editors.each do |user|
UserStats.update_contribution(:del, editors_join_table, user.id)
end
return unless parent.description_id == id
# Make sure parent doesn't point to a nonexisting object.
parent.description_id = nil
parent.save_without_our_callbacks
end
##############################################################################
# Descriptive subtitle for this description (when it is not necessary to
# include the title of the parent object), in plain text. [I'm not sure
# I like this here. It might violate MVC a bit too flagrantly... -JPH]
def put_together_name(full_or_part)
tag = :"description_#{full_or_part}_title_#{source_type}"
user_name = begin
user.legal_name
rescue StandardError
"?"
end
args = {
text: source_name,
user: user_name
}
if full_or_part == :full
args[:object] = parent.format_name
elsif source_name.present?
tag = :"#{tag}_with_text"
end
tag.l(args)
end
# Change a given User's or UserGroup's privileges.
def chg_permission(groups, arg, mode)
arg = UserGroup.one_user(arg) if arg.is_a?(User)
if (mode == :add) &&
groups.exclude?(arg)
groups.push(arg)
elsif (mode == :remove) &&
groups.include?(arg)
groups.delete(arg)
end
end
def user_made_a_change?(user)
versions.where(user_id: user.id).any?
end
# By default make first user to add any text an author.
def author_worthy?
notes?
end
end