Class: Simplec::Page

Inherits:
ApplicationRecord show all
Defined in:
app/models/simplec/page.rb

Overview

This class represents a page in the system.

Model and Template Relationship.

This is a para with code. WTF

Each page has:

  • a class located in: app/models/page/NAME.rb

  • a partial template in: app/views/pages/_NAME.html.erb

Where NAME is the demodulized, snake-case name of the Page Subclass.

Examples:

Class and template


# app/models/page/home.rb
class Page::Home < Page
  field :h1
end

<!-- app/views/pages/_home.html.erb -->
<h1>My Application</h1>
<h2><%= @page.tagline %></h2>

Direct Known Subclasses

Admin::Page

Defined Under Namespace

Classes: AlreadyLinkedEmbeddedImage

Constant Summary collapse

FILE_FIELDS =
[:file, :image].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#fieldsJSON

JSONB Postgres field that holds all defined fields. Use only if you know what you are doing.

Returns:

  • (JSON)


# File 'app/models/simplec/page.rb', line 85

#layoutString

This is the layout to be used when the page is rendered. This attribute overrides the associated Subdomain’s default_layout.

See Simplec::Subdomain#layouts to get a list of optional layouts.

Returns:

  • (String)


# File 'app/models/simplec/page.rb', line 77

#meta_descriptionString

This is the meta description tag for the page.

Returns:

  • (String)


# File 'app/models/simplec/page.rb', line 73

#pathString (readonly)

The the path is computed from the slug and the sum all parent pages.

Returns:

  • (String)


# File 'app/models/simplec/page.rb', line 65

#slugString

The value is normalized to a string starting without a leading slash and ending without a slash. Case is not changed.

Returns:

  • (String)


# File 'app/models/simplec/page.rb', line 60

#titleString

This is the title of the page.

Returns:

  • (String)


# File 'app/models/simplec/page.rb', line 69

Class Method Details

.field(name, options = {}) ⇒ Object

Define a field on the page

There is as template for each type for customization located in:

app/views/shared/fields/_TYPE.html.erb

Defines a field on a subclass. This creates a getter and setter for the name passed in. The options are used when building the administration forms.

Regular dragonfly validations are available on :file and :image fields. markevans.github.io/dragonfly/models#validations

:string - yields a text input
:text - yields a textarea
:editor - yields a summernote editor
:file - yields a file field
:image - yields a file field with image preview

Parameters:

  • name (String)

    name of field to be defined

  • options (Hash) (defaults to: {})

    field options

Options Hash (options):

  • :type (Symbol)

    one of :string (default), :text, :editor, :file, :image



143
144
145
146
147
148
149
150
151
152
# File 'app/models/simplec/page.rb', line 143

def self.field(name, options={})
  fields[name] = {name: name, type: :string}.merge(options)
  if FILE_FIELDS.member?(fields[name][:type])
    dragonfly_accessor name
    data_field :"#{name}_uid"
    data_field :"#{name}_name"
  else
    data_field(name)
  end
end

.field_names(type = nil) ⇒ Object

Return names of fields.

type: :file, is the only option



174
175
176
177
178
179
180
181
182
183
184
# File 'app/models/simplec/page.rb', line 174

def self.field_names(type=nil)
  _fields = case type
            when :file
              fields.select {|k, v| FILE_FIELDS.member?(v[:type])}
            when :textual
              fields.select {|k, v| !FILE_FIELDS.member?(v[:type])}
            else
              fields
            end
  _fields.keys
end

.fieldsHash

Returns:

  • (Hash)


155
156
157
# File 'app/models/simplec/page.rb', line 155

def self.fields
  @fields ||= Hash.new
end

.index!NilClass

Index every record.

Internally this method iterates over all pages in batches of 3.

Returns:

  • (NilClass)


227
228
229
# File 'app/models/simplec/page.rb', line 227

def self.index!
  find_each(batch_size: 3) { |page| page.index! }
end

.search(term, options = {}) ⇒ ActiveRecord::Relation

If the term is nil or blank, all results will be returned.

Parameters:

  • term (String)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :types (Class, Array, String)

    A Class or Array of Classes (of page types, typically Page::Home) to limit results to.

  • all (Symbol, String)

    other options All other options are matched to the query JSONB field. These are all direct matches on the indexed query field. If you want to do anything more complicated, append it to the scope returned.

Returns:

  • (ActiveRecord::Relation)

    a relation for the query



106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'app/models/simplec/page.rb', line 106

scope :search, ->(term, options={}) {
  _types      = Array(options.delete(:types))
  _subdomains = Array(options.delete(:subdomains))

  query = all
  query = query.includes(:subdomain).
    where(simplec_subdomains: {name: _subdomains}) if _subdomains.any?
  query = query.where(type: _types) if _types.any?
  options.each { |k,v| query = query.where("query->>:k = :v", k: k, v: v) }

  if term.blank?
    query
  else
    tsq = tsquery term
    query.where("tsv @@ #{tsq}").order("ts_rank_cd(tsv, #{tsq}) DESC")
  end
}

.search_query_attributesArray

Get extra attributes on the record for querying.

See #search_query_attributes! for more information.

Returns:

  • (Array)

    of attributes



218
219
220
# File 'app/models/simplec/page.rb', line 218

def self.search_query_attributes
  @_search_query_attrs = Set.new(@_search_query_attrs).add(:id).to_a
end

.search_query_attributes!(*args) ⇒ Object

Set extra attributes on the record for querying.

Examples:

set attributes

class Page::Home < Page
  has_many :tags

  field :category

  # Where category is a Simplec::Page::field and tags is a defined
  # method.
  search_query_attributes! :category, :tags

  def tags
    self.tags.pluck(:name)
  end
end

# Built-in matching
Page.search('foo', category: 'how-to')

# Manual matching
Page.search('bar').where("query->>'tags' IN ('home', 'garden')")


209
210
211
# File 'app/models/simplec/page.rb', line 209

def self.search_query_attributes!(*args)
  @_search_query_attrs = args.map(&:to_sym)
end

.tsquery(input, options = {}) ⇒ String

Create a to_tsquery statement.

Mainly used internally, but could be used in custom queries.

Parameters:

  • input (String)

    string to be queried

  • options (Hash) (defaults to: {})

    optional

Options Hash (options):

  • :language (String)

    defaults to ‘english’ This is really a future addition, all of the tsvector fields are set to ‘english’.

Returns:

  • (String)

    a to_tsquery statement



242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
# File 'app/models/simplec/page.rb', line 242

def self.tsquery(input, options={})
  options[:language] ||= 'english'
  value = input.to_s.strip
  value  = value.
    gsub('(', '').
    gsub(')', '').
    gsub(%q('), '').
    gsub(' ', '\\ ').
    gsub(':', '').
    gsub("\t", '').
    gsub("!", '')
  value << ':*'
  query = "to_tsquery(?, ?)"
  sanitize_sql_array([query, options[:language], value])
end

.type(type) ⇒ Class

Return a constantized type, whitelisted by known subclasses.

Returns:

  • (Class)

Raises:

  • (RuntimeError)

    if Page subclass isn’t defined.



163
164
165
166
167
168
# File 'app/models/simplec/page.rb', line 163

def self.type(type)
  ::Page rescue raise '::Page not defined, define it in app/models'
  raise 'Unsupported Page Type; define in app/models/page/' unless ::Page.subclasses.map(&:name).
    member?(type)
  type.constantize
end

Instance Method Details

#build_pathString

Build the path of the page to be used in routing.

Used as a before validation hook.

Returns:

  • (String)

    the computed path for the page



282
283
284
285
# File 'app/models/simplec/page.rb', line 282

def build_path
  _pages = self.parents.reverse + [self]
  self.path = _pages.map(&:slug).reject(&:blank?).join('/')
end

#extract_search_text(*attributes) ⇒ String

Extract text out of HTML or plain strings. Basically removes html formatting.

Parameters:

  • attributes (Symbol, String)

    variable list of attributes or methods to be extracted for search

Returns:

  • (String)

    content of each attribute separated by new lines



355
356
357
358
359
360
# File 'app/models/simplec/page.rb', line 355

def extract_search_text(*attributes)
  Array(attributes).map { |meth|
    Nokogiri::HTML(self.send(meth)).xpath("//text()").
      map {|node| text = node.text; text.try(:strip!); text}.join(" ")
  }.reject(&:blank?).join("\n")
end

#field_optionsObject

Return field options for building forms.



260
261
262
# File 'app/models/simplec/page.rb', line 260

def field_options
  self.class.fields.values
end

#find_embedded_imagesArray

Search all of the fields text and create an array of all found Simplec::EmbeddedImages.

Returns:

  • (Array)

    of Simplec::EmbeddedImages



304
305
306
307
308
309
310
# File 'app/models/simplec/page.rb', line 304

def find_embedded_images
  text = self.fields.values.join(' ')
  matches = text.scan(/ei=([^&]*)/)
  encoded_ids = matches.map(&:first)
  ids = encoded_ids.map { |eid| Base64.urlsafe_decode64(URI.unescape(eid)) }
  EmbeddedImage.includes(:embeddable).find(ids)
end

#index!Boolean

Index this record for search.

Internally, this method uses update_columns so it can be used in after_save callbacks, etc.

Returns:

  • (Boolean)

    success



342
343
344
345
346
# File 'app/models/simplec/page.rb', line 342

def index!
  set_search_text!
  set_query_attributes!
  update_columns text: self.text, query: self.query
end

#layoutsArray

Get layout options.

See Simplec::Subdomain#layouts.

Returns:

  • (Array)

    of layout String names



332
333
334
# File 'app/models/simplec/page.rb', line 332

def layouts
  @layouts ||= Subdomain.new.layouts
end

Set this instance as the #embeddable association on the

Simplec::EmbeddedImage

Used as an after_save hook.

Returns:

  • (Array)

    of Simplec::EmbeddedImages



318
319
320
321
322
323
324
325
# File 'app/models/simplec/page.rb', line 318

def link_embedded_images!
  images = self.find_embedded_images
  images.each do |image|
    raise AlreadyLinkedEmbeddedImage if image.embeddable &&
      image.embeddable != self
    image.update!(embeddable: self)
  end
end

#match_parent_subdomainSimplec::Subdomain

Sets the #subdomain t that of the parent.

All pages need to have a matching subdomain to parent page. Does nothing if there is no parent.

Used as a before validation hook.

Returns:



295
296
297
298
# File 'app/models/simplec/page.rb', line 295

def match_parent_subdomain
  return unless self.parent
  self.subdomain = self.parent.subdomain
end

#parentsObject

List parents, closest to furthest.

This is a recursive, expensive call.



268
269
270
271
272
273
274
275
# File 'app/models/simplec/page.rb', line 268

def parents
  page, parents = self, Array.new
  while page.parent
    page = page.parent
    parents << page
  end
  parents
end

#set_query_attributes!Hash

Build query attribute hash.

Internally stored as JSONB.

Returns:

  • (Hash)

    to be set for query attribute



384
385
386
387
388
389
390
# File 'app/models/simplec/page.rb', line 384

def set_query_attributes!
  attr_names = self.class.search_query_attributes.map(&:to_s)
  self.query = attr_names.inject({}) { |memo, attr|
    memo[attr] = self.send(attr)
    memo
  }
end

#set_search_text!Object

Set the text which will be index.

a title, meta_description b slug, path (non-printable, add tags, added terms) c textual fields d (reserved for sub-records, etc)

‘a’ correlates to ‘A’ priority in Postgresql. For more information: www.postgresql.org/docs/9.6/static/functions-textsearch.html



372
373
374
375
376
377
# File 'app/models/simplec/page.rb', line 372

def set_search_text!
  self.text['a'] = extract_search_text :title, :meta_description
  self.text['b'] = extract_search_text :slug, :path
  self.text['c'] = extract_search_text *self.class.field_names(:textual)
  self.text['d'] = nil
end