simplec 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +258 -0
  4. data/Rakefile +36 -0
  5. data/app/assets/config/simplec_manifest.js +2 -0
  6. data/app/assets/fonts/simplec/summernote.eot +0 -0
  7. data/app/assets/fonts/simplec/summernote.ttf +0 -0
  8. data/app/assets/fonts/simplec/summernote.woff +0 -0
  9. data/app/assets/javascripts/simplec/application.js +13 -0
  10. data/app/assets/javascripts/simplec/summernote-config.js +82 -0
  11. data/app/assets/javascripts/simplec/summernote.js +7046 -0
  12. data/app/assets/stylesheets/simplec/application.css +15 -0
  13. data/app/assets/stylesheets/simplec/summernote.scss +691 -0
  14. data/app/constraints/simplec/subdomains.rb +23 -0
  15. data/app/controllers/simplec/application_controller.rb +9 -0
  16. data/app/controllers/simplec/pages_controller.rb +19 -0
  17. data/app/helpers/simplec/application_helper.rb +4 -0
  18. data/app/jobs/simplec/application_job.rb +4 -0
  19. data/app/mailers/simplec/application_mailer.rb +6 -0
  20. data/app/models/simplec/application_record.rb +5 -0
  21. data/app/models/simplec/embedded_image.rb +15 -0
  22. data/app/models/simplec/page.rb +203 -0
  23. data/app/models/simplec/subdomain.rb +30 -0
  24. data/app/views/simplec/fields/_editor.html.erb +5 -0
  25. data/app/views/simplec/fields/_image.html.erb +14 -0
  26. data/app/views/simplec/fields/_string.html.erb +4 -0
  27. data/app/views/simplec/fields/_text.html.erb +4 -0
  28. data/app/views/simplec/pages/show.html.erb +2 -0
  29. data/config/routes.rb +8 -0
  30. data/db/migrate/20170809204353_create_simplec_pages.rb +30 -0
  31. data/db/migrate/20170809204511_create_simplec_subdomains.rb +12 -0
  32. data/db/migrate/20170809210304_create_simplec_embedded_images.rb +15 -0
  33. data/lib/simplec.rb +5 -0
  34. data/lib/simplec/action_controller/extensions.rb +56 -0
  35. data/lib/simplec/action_view/helper.rb +118 -0
  36. data/lib/simplec/embedded_image_actions.rb +37 -0
  37. data/lib/simplec/engine.rb +18 -0
  38. data/lib/simplec/version.rb +3 -0
  39. data/lib/tasks/simplec_tasks.rake +4 -0
  40. metadata +166 -0
@@ -0,0 +1,23 @@
1
+ module Simplec
2
+ class Subdomains
3
+ def self.matches?(request)
4
+ present = request.subdomain.present?
5
+ not_admin = request.subdomain != 'admin'
6
+ subdomain = Simplec::Subdomain.find_by(name: request.subdomain)
7
+
8
+ match = present && not_admin && subdomain
9
+
10
+ if match
11
+ Thread.current[:simplec_subdomain] = subdomain
12
+ else
13
+ Rails.logger.info <<-LOG unless match
14
+ Simplec Subdomain '#{request.subdomain}' was not found.
15
+ ActionDispatch::Request#original_url: #{request.original_url}
16
+ 'admin' subdomain bypass: #{!not_admin}
17
+ LOG
18
+ end
19
+
20
+ match
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,9 @@
1
+ module Simplec
2
+ class ApplicationController < ::ActionController::Base
3
+ protect_from_forgery with: :exception
4
+
5
+ after_action do
6
+ Thread.current[:simplec_subdomain] = nil
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,19 @@
1
+ require_dependency "simplec/application_controller"
2
+
3
+ module Simplec
4
+ class PagesController < ApplicationController
5
+
6
+ def show
7
+ @page = page("/#{params[:path]}")
8
+ render layout: layout(@page)
9
+ end
10
+
11
+ private
12
+
13
+ def layout(page)
14
+ [page.layout, page.subdomain.default_layout, 'public']. # TODO allow config for public
15
+ reject(&:blank?).first
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ module Simplec
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Simplec
2
+ class ApplicationJob < ActiveJob::Base
3
+ end
4
+ end
@@ -0,0 +1,6 @@
1
+ module Simplec
2
+ class ApplicationMailer < ActionMailer::Base
3
+ default from: 'from@example.com'
4
+ layout 'mailer'
5
+ end
6
+ end
@@ -0,0 +1,5 @@
1
+ module Simplec
2
+ class ApplicationRecord < ActiveRecord::Base
3
+ self.abstract_class = true
4
+ end
5
+ end
@@ -0,0 +1,15 @@
1
+ module Simplec
2
+ class EmbeddedImage < ApplicationRecord
3
+ belongs_to :embeddable,
4
+ polymorphic: true
5
+
6
+ dragonfly_accessor :asset
7
+
8
+ def url
9
+ return unless self.asset
10
+ return self.asset.url unless persisted?
11
+ self.asset.url(ei: Base64.urlsafe_encode64(self.id.to_s))
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,203 @@
1
+ # Simplec::Page
2
+ #
3
+ # This class represents a page in the system.
4
+ #
5
+ # Each page has a class located in:
6
+ # app/models/page/NAME.html.erb
7
+ #
8
+ # Each page has a partial template in:
9
+ # app/views/pages/_NAME.html.erb
10
+ #
11
+ # Where NAME is the demodulized, snake-case name of the Page Subclasss. For
12
+ # example:
13
+ #
14
+ # class Page::Home < Page
15
+ # field :h1
16
+ # end
17
+ #
18
+ # Would live in app/models/page/home.html.erb and have a template in
19
+ # app/views/pages/_home.html.erb.
20
+ #
21
+ # Each page has the following attributes by default:
22
+ # - page.title
23
+ #
24
+ # Read the documentation below for the class method .field, it is the most
25
+ # used method.
26
+ #
27
+ #
28
+ module Simplec
29
+ class Page < ApplicationRecord
30
+
31
+
32
+ # TODO Document FILE_FIELDS constant
33
+ #
34
+ FILE_FIELDS = [:file, :image].freeze
35
+
36
+ belongs_to :subdomain,
37
+ optional: false
38
+ belongs_to :parent,
39
+ class_name: 'Page',
40
+ optional: true
41
+ has_many :childern,
42
+ class_name: 'Page',
43
+ foreign_key: :parent_id
44
+ has_many :embedded_images,
45
+ as: :embeddable,
46
+ dependent: :delete_all
47
+
48
+ validates :type,
49
+ presence: true
50
+ validates :path,
51
+ presence: true,
52
+ uniqueness: { scope: :subdomain_id }
53
+ validates :layout,
54
+ inclusion: {in: :layouts, allow_blank: true}
55
+ validates :title,
56
+ presence: true
57
+
58
+ before_validation :match_parent_subdomain
59
+ before_validation :build_path
60
+ after_save :link_embedded_images!
61
+
62
+ # Define a field on the page.
63
+ #
64
+ # - name - name of field
65
+ # - options[:type] - :string (default), :text, :editor, [:file], :image
66
+ # :string - yields a text input
67
+ # :text - yields a textarea
68
+ # :editor - yields a summernote editor
69
+ # :file - yields a file field
70
+ # :image - yields a file field with image preview
71
+ #
72
+ # There is as template for each type for customization located in:
73
+ # app/views/shared/fields/_TYPE.html.erb
74
+ #
75
+ # Defines a field on a subclass. This creates a getter and setter for the
76
+ # name passed in. The options are used when building the administration forms.
77
+ #
78
+ # Regular dragonfly validations are available on :file and :image fields.
79
+ # http://markevans.github.io/dragonfly/models#validations
80
+ #
81
+ def self.field(name, options={})
82
+ fields[name] = {name: name, type: :string}.merge(options)
83
+ if FILE_FIELDS.member?(fields[name][:type])
84
+ dragonfly_accessor name
85
+ data_field :"#{name}_uid"
86
+ data_field :"#{name}_name"
87
+ else
88
+ data_field(name)
89
+ end
90
+ end
91
+
92
+ # List fields
93
+ #
94
+ def self.fields
95
+ @fields ||= Hash.new
96
+ end
97
+
98
+ # Return a constantized type, whitelisted by known subclasses.
99
+ #
100
+ def self.type(type)
101
+ ::Page rescue raise '::Page not defined, define it in app/models'
102
+ raise 'Unsupported Page Type; define in app/models/page/' unless ::Page.subclasses.map(&:name).
103
+ member?(type)
104
+ type.constantize
105
+ end
106
+
107
+ # Return names of fields.
108
+ #
109
+ # type: :file, is the only option
110
+ #
111
+ def self.field_names(type=nil)
112
+ _fields = case type
113
+ when :file
114
+ fields.select {|k, v| FILE_FIELDS.member?(v[:type])}
115
+ else
116
+ fields
117
+ end
118
+ _fields.keys
119
+ end
120
+
121
+ # Return field options for building forms.
122
+ #
123
+ def field_options
124
+ self.class.fields.values
125
+ end
126
+
127
+ # List parents, closest to furthest.
128
+ #
129
+ def parents
130
+ page, parents = self, Array.new
131
+ while page.parent
132
+ page = page.parent
133
+ parents << page
134
+ end
135
+ parents
136
+ end
137
+
138
+ # Before validation hook.
139
+ #
140
+ # Build the path of the page to be used in routing.
141
+ #
142
+ def build_path
143
+ _pages = self.parents.reverse + [self]
144
+ self.path = "/#{_pages.map(&:slug).reject(&:blank?).join('/')}"
145
+ end
146
+
147
+ # Before validation hook
148
+ #
149
+ # All pages need to have a matching subdomain to parent page
150
+ #
151
+ def match_parent_subdomain
152
+ return unless self.parent
153
+ self.subdomain = self.parent.subdomain
154
+ end
155
+
156
+ def find_embedded_images
157
+ text = self.fields.values.join(' ')
158
+ matches = text.scan(/ei=([^&]*)/)
159
+ encoded_ids = matches.map(&:first)
160
+ ids = encoded_ids.map { |eid| Base64.urlsafe_decode64(URI.unescape(eid)) }
161
+ EmbeddedImage.includes(:embeddable).find(ids)
162
+ end
163
+
164
+ def link_embedded_images!
165
+ images = self.find_embedded_images
166
+ images.each do |image|
167
+ raise AlreadyLinkedEmbeddedImage if image.embeddable &&
168
+ image.embeddable != self
169
+ image.update!(embeddable: self)
170
+ end
171
+ end
172
+
173
+ def layouts
174
+ @layouts ||= Subdomain.new.layouts
175
+ end
176
+
177
+ module Normalizers
178
+ def slug=(val)
179
+ val = val ? val.to_s.split('/').reject(&:blank?).join('/') : val
180
+ super val
181
+ end
182
+
183
+ def slug
184
+ if self.parent_id && self.parent.nil?
185
+ self.path.to_s.split('/').reject(&:blank?).join('/')
186
+ else
187
+ super
188
+ end
189
+ end
190
+ end
191
+ prepend Normalizers
192
+
193
+ class AlreadyLinkedEmbeddedImage < StandardError; end
194
+
195
+ private
196
+
197
+ def self.data_field(name)
198
+ define_method(name) { fields[name.to_s] }
199
+ define_method("#{name}=") { |val| fields[name.to_s] = val }
200
+ end
201
+ end
202
+ end
203
+
@@ -0,0 +1,30 @@
1
+ module Simplec
2
+ class Subdomain < ApplicationRecord
3
+ has_many :pages
4
+ has_and_belongs_to_many :document_sets
5
+ has_and_belongs_to_many :documents
6
+
7
+ validates :name,
8
+ presence: true,
9
+ exclusion: { in: %w(admin) }
10
+ validates :default_layout,
11
+ inclusion: {in: :layouts, allow_blank: true}
12
+
13
+ def layouts
14
+ @layouts ||= Dir[Rails.root.join('app/views/layouts').to_s + "/*.html.*"].
15
+ map{|n| File.basename(n).split('.', 2).first }.
16
+ reject{|n| n =~ /\A_/ || n =~ /mailer/ || n =~ /application/ || n =~ /sessions/}.
17
+ sort
18
+ end
19
+
20
+ module Normalizers
21
+
22
+ # Force lowercase name
23
+ #
24
+ def name=(val)
25
+ super (val ? val.to_s.strip.downcase : val)
26
+ end
27
+ end
28
+ prepend Normalizers
29
+ end
30
+ end
@@ -0,0 +1,5 @@
1
+ <div class="form-group">
2
+ <%= f.label name, class: 'control-label' %>
3
+ <%= f.hidden_field name, id: "#{name}-#{f.object.object_id}"%>
4
+ <%= tag :div, class: 'editor-field', data: {input: "##{name}-#{f.object.object_id}"} %></div>
5
+ </div>
@@ -0,0 +1,14 @@
1
+ <div class="form-group">
2
+ <% if f.object.send(name) %>
3
+ <%= image_tag f.object.send(name).thumb('100x').url %>
4
+ <div class="checkbox">
5
+ <label>
6
+ <%= f.check_box :"remove_#{name}" %>
7
+ Remove
8
+ </label>
9
+ </div>
10
+ <% end %>
11
+ <%= f.label name %>
12
+ <%= f.file_field name %>
13
+ <%= f.hidden_field :"retained_#{name}" %>
14
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="form-group">
2
+ <%= f.label name, class: 'control-label' %>
3
+ <%= f.text_field name, class: 'form-control' %>
4
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="form-group">
2
+ <%= f.label name, class: 'control-label' %>
3
+ <%= f.text_area name, class: 'form-control' %>
4
+ </div>
@@ -0,0 +1,2 @@
1
+ <%= head @page %>
2
+ <%= template @page %>
@@ -0,0 +1,8 @@
1
+ Simplec::Engine.routes.draw do
2
+
3
+ scope constraints: Simplec::Subdomains do
4
+ root 'pages#show'
5
+ get '*path', to: 'pages#show'
6
+ end
7
+
8
+ end
@@ -0,0 +1,30 @@
1
+ class CreateSimplecPages < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :simplec_pages, id: :uuid, default: 'gen_random_uuid()' do |t|
4
+ t.string :type
5
+ t.uuid :subdomain_id
6
+ t.uuid :parent_id
7
+ t.string :path
8
+ t.string :slug
9
+ t.string :title
10
+ t.string :meta_description
11
+ t.jsonb :fields
12
+ t.string :layout
13
+
14
+ t.timestamps
15
+ end
16
+
17
+ add_index :simplec_pages, :type
18
+ add_index :simplec_pages, :subdomain_id
19
+ add_index :simplec_pages, :parent_id
20
+ add_index :simplec_pages, :path
21
+ add_index :simplec_pages, [:subdomain_id, :path], unique: true
22
+
23
+ reversible do |dir|
24
+ dir.up {
25
+ execute "ALTER TABLE simplec_pages ALTER COLUMN fields SET DEFAULT '{}'::JSONB"
26
+ }
27
+ end
28
+
29
+ end
30
+ end
@@ -0,0 +1,12 @@
1
+ class CreateSimplecSubdomains < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :simplec_subdomains, id: :uuid, default: 'gen_random_uuid()' do |t|
4
+ t.string :name
5
+ t.string :default_layout
6
+
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :simplec_subdomains, :name, unique: true
11
+ end
12
+ end
@@ -0,0 +1,15 @@
1
+ class CreateSimplecEmbeddedImages < ActiveRecord::Migration[5.0]
2
+ def change
3
+ create_table :simplec_embedded_images, id: :uuid, default: 'gen_random_uuid()' do |t|
4
+ t.string :embeddable_type
5
+ t.integer :embeddable_id
6
+ t.string :asset_uid
7
+ t.string :asset_name
8
+
9
+ t.timestamps
10
+ end
11
+
12
+ add_index :simplec_embedded_images, [:embeddable_type, :embeddable_id],
13
+ name: 'simplec_embedded_images_type_id_index'
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ require "simplec/engine"
2
+
3
+ module Simplec
4
+ # TODO configuration options
5
+ end