templatr 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +34 -0
- data/app/assets/javascripts/templatr/application.js +13 -0
- data/app/assets/stylesheets/templatr/application.css +13 -0
- data/app/controllers/templatr/application_controller.rb +4 -0
- data/app/helpers/templatr/application_helper.rb +4 -0
- data/app/models/templatr/field.rb +168 -0
- data/app/models/templatr/field_group.rb +5 -0
- data/app/models/templatr/field_value.rb +13 -0
- data/app/models/templatr/tag.rb +120 -0
- data/app/models/templatr/tag_field_value.rb +6 -0
- data/app/models/templatr/template.rb +54 -0
- data/app/views/layouts/templatr/application.html.erb +14 -0
- data/config/routes.rb +2 -0
- data/lib/tasks/templatr_tasks.rake +4 -0
- data/lib/templatr/acts_as_templatable.rb +154 -0
- data/lib/templatr/engine.rb +11 -0
- data/lib/templatr/version.rb +3 -0
- data/lib/templatr.rb +4 -0
- data/test/dummy/README.rdoc +28 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/assets/javascripts/application.js +13 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +5 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/bin/bundle +3 -0
- data/test/dummy/bin/rails +4 -0
- data/test/dummy/bin/rake +4 -0
- data/test/dummy/config/application.rb +23 -0
- data/test/dummy/config/boot.rb +5 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +29 -0
- data/test/dummy/config/environments/production.rb +80 -0
- data/test/dummy/config/environments/test.rb +36 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/test/dummy/config/initializers/inflections.rb +16 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +12 -0
- data/test/dummy/config/initializers/session_store.rb +3 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +23 -0
- data/test/dummy/config/routes.rb +4 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/public/404.html +58 -0
- data/test/dummy/public/422.html +58 -0
- data/test/dummy/public/500.html +57 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/integration/navigation_test.rb +10 -0
- data/test/templatr_test.rb +7 -0
- data/test/test_helper.rb +15 -0
- metadata +160 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 32041fe62ad050adc8cf4e9f0597cb11860a97f3
|
4
|
+
data.tar.gz: c563d911b5beec9fb6ac262a327cc22ea5e1c2de
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: becc08187da3c2adff63e823e26667659506928e9305ce0a1b50185183624e9337a72ba6be4184bfacbdd0810cc6c8b376a4277c37bcb34de509a9cf1f0f631d
|
7
|
+
data.tar.gz: f6d84d360119094beeba9ded075ce15ba5128403df732dfe69d6f88bf151bfa33aca8cd5878e632a3bb2552794315f7e7f564e4bc1e45ae16d259d0cdaa0eb85
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Templatr'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../test/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
Bundler::GemHelper.install_tasks
|
23
|
+
|
24
|
+
require 'rake/testtask'
|
25
|
+
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
27
|
+
t.libs << 'lib'
|
28
|
+
t.libs << 'test'
|
29
|
+
t.pattern = 'test/**/*_test.rb'
|
30
|
+
t.verbose = false
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
task default: :test
|
@@ -0,0 +1,13 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into application.js, which will include all the files
|
2
|
+
// listed below.
|
3
|
+
//
|
4
|
+
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
|
5
|
+
// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
|
6
|
+
//
|
7
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
8
|
+
// compiled file.
|
9
|
+
//
|
10
|
+
// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
|
11
|
+
// about supported directives.
|
12
|
+
//
|
13
|
+
//= require_tree .
|
@@ -0,0 +1,13 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
3
|
+
* listed below.
|
4
|
+
*
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
6
|
+
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
|
7
|
+
*
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the top of the
|
9
|
+
* compiled file, but it's generally better to create a new file per style scope.
|
10
|
+
*
|
11
|
+
*= require_self
|
12
|
+
*= require_tree .
|
13
|
+
*/
|
@@ -0,0 +1,168 @@
|
|
1
|
+
module Templatr
|
2
|
+
class Field < ActiveRecord::Base
|
3
|
+
has_many :field_values, :dependent => :destroy
|
4
|
+
has_many :tags, :inverse_of => :field
|
5
|
+
|
6
|
+
belongs_to :field_group
|
7
|
+
|
8
|
+
scope :common, where(:template_id => nil)
|
9
|
+
scope :specific, where("template_id IS NOT NULL")
|
10
|
+
scope :common_or_specific_type, lambda {|type| where("template_id IS NULL OR template_id = ?", type.id) }
|
11
|
+
scope :show_tag_cloud, where('show_tag_cloud').order('"order" ASC, id ASC')
|
12
|
+
scope :included_in_item_list, where('include_in_item_list').order('"order" ASC, id ASC')
|
13
|
+
scope :with_name, lambda {|name| where("LOWER(name) = LOWER(?) ", name) }
|
14
|
+
|
15
|
+
def self.search_suggestions(value = true); where(:search_suggestions => value); end
|
16
|
+
|
17
|
+
def self.templatable_class
|
18
|
+
self.to_s.gsub(/Field\Z/, '').constantize
|
19
|
+
end
|
20
|
+
|
21
|
+
# Reserved field names that the user is not allowed to use
|
22
|
+
# "Type" is reserved to differentiate between Templates
|
23
|
+
def self.reserved_fields
|
24
|
+
[:type] + templatable_class.attribute_names.collect(&:to_sym) + templatable_class.reflect_on_all_associations.collect(&:name)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.valid_field_types
|
28
|
+
%w(string text float integer boolean integer_with_uncertainty select_one select_multiple)
|
29
|
+
end
|
30
|
+
|
31
|
+
# The valid ways to migrate data between field types
|
32
|
+
def self.valid_migration_paths
|
33
|
+
{ :string => [:select_one, :select_multiple, :text],
|
34
|
+
:select_one => [:string, :select_multiple, :text]
|
35
|
+
}.with_indifferent_access
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.valid_migration_path?(from, to)
|
39
|
+
paths = valid_migration_paths[from]
|
40
|
+
paths && paths.include?(to.to_sym)
|
41
|
+
end
|
42
|
+
|
43
|
+
def valid_migration_paths
|
44
|
+
new_record? ? self.class.valid_field_types : [self.field_type] + Array(self.class.valid_migration_paths[self.field_type])
|
45
|
+
end
|
46
|
+
|
47
|
+
accepts_nested_attributes_for :field_values, :allow_destroy => true, :reject_if => :all_blank
|
48
|
+
|
49
|
+
validates_inclusion_of :field_type, :in => valid_field_types
|
50
|
+
|
51
|
+
validates_presence_of :name
|
52
|
+
validate :has_unique_name
|
53
|
+
validates_exclusion_of :name, :in => lambda {|f| CSVSerializer.han(f.class.templatable_class, f.class.reserved_fields, :downcase => true) }
|
54
|
+
|
55
|
+
after_save :disambiguate_fields, :migrate_field_type
|
56
|
+
after_destroy :disambiguate_fields
|
57
|
+
|
58
|
+
def string?; field_type == 'string' end
|
59
|
+
def text?; field_type == 'text' end
|
60
|
+
def boolean?; field_type == 'boolean' end
|
61
|
+
def float?; field_type == 'float' end
|
62
|
+
def integer?; field_type == 'integer' end
|
63
|
+
def integer_with_uncertainty?; field_type == 'integer_with_uncertainty' end
|
64
|
+
def select?; field_type == 'select_one' || field_type == 'select_multiple' end
|
65
|
+
def select_one?; field_type == 'select_one' end
|
66
|
+
def select_multiple?; field_type == 'select_multiple' end
|
67
|
+
|
68
|
+
def to_s
|
69
|
+
self.name
|
70
|
+
end
|
71
|
+
|
72
|
+
def common?
|
73
|
+
template_id.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
def facet?
|
77
|
+
include_in_search_form? || search_suggestions?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Don't allow changes to the type if the field is saved
|
81
|
+
def can_change_type?
|
82
|
+
new_record? || self.class.valid_migration_paths[self.field_type].present?
|
83
|
+
end
|
84
|
+
|
85
|
+
def scalar?
|
86
|
+
string? || text? || boolean? || float? || integer? || integer_with_uncertainty?
|
87
|
+
end
|
88
|
+
|
89
|
+
def vector?
|
90
|
+
!scalar?
|
91
|
+
end
|
92
|
+
|
93
|
+
# Coerce the field_type to a string at all times so testing for it is easier
|
94
|
+
def field_type=(value)
|
95
|
+
super(value.to_s)
|
96
|
+
end
|
97
|
+
|
98
|
+
# GLINT INTEGRATION
|
99
|
+
|
100
|
+
# What attribute type should glint use to store this field's values
|
101
|
+
def attribute_type
|
102
|
+
(float? || integer? || text? || boolean? ? field_type : 'string').to_sym
|
103
|
+
end
|
104
|
+
|
105
|
+
def facet_name
|
106
|
+
:"field_#{id}"
|
107
|
+
end
|
108
|
+
|
109
|
+
def param
|
110
|
+
(self.disambiguate? ? "#{template.name} #{self.name}" : self.name).downcase # param is always case insensitive
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def migrate_field_type
|
116
|
+
return unless field_type_changed? && field_type_was.present?
|
117
|
+
|
118
|
+
if self.class.valid_migration_path?(field_type_was, self.field_type)
|
119
|
+
new_field_type = self.field_type
|
120
|
+
self.field_type = field_type_was
|
121
|
+
|
122
|
+
tags.collect do |tag|
|
123
|
+
[tag, tag.value]
|
124
|
+
end.tap do
|
125
|
+
self.field_type = new_field_type
|
126
|
+
end.each do |tag, old_value|
|
127
|
+
tag.value = old_value
|
128
|
+
tag.save!
|
129
|
+
end
|
130
|
+
|
131
|
+
# Unhook the old field values
|
132
|
+
unless select?
|
133
|
+
tags.each do |tag|
|
134
|
+
tag.update_attribute(:field_value, nil)
|
135
|
+
tag.field_values = []
|
136
|
+
end
|
137
|
+
field_values.destroy_all
|
138
|
+
end
|
139
|
+
else
|
140
|
+
raise "Can't convert from a #{field_type_was} to #{field_type} field"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def has_unique_name
|
145
|
+
scope = self.class.where("LOWER(name) = LOWER(?)", self.name)
|
146
|
+
scope = scope.where("id != ?", self.id) if self.id
|
147
|
+
scope = scope.where(:template_id => [nil, self.template_id]) if self.template_id
|
148
|
+
|
149
|
+
errors.add(:name, "has already been taken") if scope.exists?
|
150
|
+
end
|
151
|
+
|
152
|
+
# Finds all fields with the same name and ensures they know there is another field with the same name
|
153
|
+
# thus allowing us to have them a prefix that lets us identify them in a query string
|
154
|
+
def disambiguate_fields
|
155
|
+
if name_changed? # New, Updated
|
156
|
+
fields = self.class.specific.where("LOWER(name) = LOWER(?)", self.name)
|
157
|
+
fields.update_all(:disambiguate => fields.many?)
|
158
|
+
end
|
159
|
+
|
160
|
+
if name_was # Updated, Destroyed
|
161
|
+
fields = self.class.specific.where("LOWER(name) = LOWER(?)", self.name_was)
|
162
|
+
fields.update_all(:disambiguate => fields.many?)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# END GLINT INTEGRATION
|
167
|
+
end
|
168
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Templatr
|
2
|
+
class FieldValue < ActiveRecord::Base
|
3
|
+
belongs_to :field
|
4
|
+
|
5
|
+
has_many :tag_field_values # Don't need to destroy this because tags will take care of the link tables
|
6
|
+
|
7
|
+
validates_presence_of :field_id, :value
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
self.value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
module Templatr
|
3
|
+
class Tag < ActiveRecord::Base
|
4
|
+
belongs_to :field, :inverse_of => :tags
|
5
|
+
|
6
|
+
# Select
|
7
|
+
belongs_to :field_value
|
8
|
+
|
9
|
+
# Select Multiple
|
10
|
+
has_many :tag_field_values, :dependent => :destroy
|
11
|
+
has_many :field_values, :through => :tag_field_values
|
12
|
+
|
13
|
+
delegate :scalar?, :vector?, :string?, :text?, :select_one?, :select_multiple?, :boolean?, :float?, :integer?, :integer_with_uncertainty?, :to => :field
|
14
|
+
|
15
|
+
before_validation :mark_for_destruction_if_blank
|
16
|
+
|
17
|
+
attr_writer :value # Allows the setting and getting of the value, before it has been persisted
|
18
|
+
before_save :persist_value
|
19
|
+
|
20
|
+
def self.templatable_class
|
21
|
+
self.to_s[/[A-Z][a-z]+/].constantize
|
22
|
+
end
|
23
|
+
|
24
|
+
def custom_tag?
|
25
|
+
!field
|
26
|
+
end
|
27
|
+
|
28
|
+
def name
|
29
|
+
custom_tag? ? self['name'] : field.name
|
30
|
+
end
|
31
|
+
|
32
|
+
def value
|
33
|
+
if !@value.nil?
|
34
|
+
@value
|
35
|
+
elsif custom_tag? || string?
|
36
|
+
string_value
|
37
|
+
elsif text?
|
38
|
+
text_value
|
39
|
+
elsif select_one?
|
40
|
+
field_value.to_s
|
41
|
+
elsif select_multiple?
|
42
|
+
field_values.collect(&:to_s)
|
43
|
+
elsif boolean?
|
44
|
+
boolean_value
|
45
|
+
elsif float?
|
46
|
+
float_value
|
47
|
+
elsif integer?
|
48
|
+
integer_value
|
49
|
+
elsif integer_with_uncertainty?
|
50
|
+
_value = integer_value.to_s
|
51
|
+
_value << " ± #{integer_value_uncertainty}" if integer_value_uncertainty
|
52
|
+
_value
|
53
|
+
else
|
54
|
+
raise "Unknown Field Type: #{field.field_type.inspect}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
value.is_a?(Array) ? value.join(', ') : value
|
60
|
+
end
|
61
|
+
|
62
|
+
def field_group_id
|
63
|
+
field.field_group_id if field
|
64
|
+
end
|
65
|
+
|
66
|
+
# Allow field value to be set by passing a string
|
67
|
+
def field_value=(value)
|
68
|
+
super find_or_create_field_value(value)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Allow field value to be set by passing a string
|
72
|
+
def field_values=(value)
|
73
|
+
super Array.wrap(value).collect {|value| find_or_create_field_value(value) }
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def find_or_create_field_value(value)
|
79
|
+
case value
|
80
|
+
when String
|
81
|
+
field.field_values.where(:value => value).first_or_create!
|
82
|
+
else
|
83
|
+
value
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def persist_value
|
88
|
+
if custom_tag? || string?
|
89
|
+
self.string_value = @value.to_s # Ensure that if an AR object is passed, it doesn't turn into the record id
|
90
|
+
elsif text?
|
91
|
+
self.text_value = @value.to_s
|
92
|
+
elsif select_one?
|
93
|
+
self.field_value = @value
|
94
|
+
elsif select_multiple?
|
95
|
+
self.field_values = @value
|
96
|
+
elsif boolean?
|
97
|
+
self.boolean_value = @value
|
98
|
+
elsif float?
|
99
|
+
self.float_value = @value.to_s
|
100
|
+
elsif integer?
|
101
|
+
self.integer_value = @value.to_s
|
102
|
+
elsif integer_with_uncertainty?
|
103
|
+
self.integer_value = @value.first.to_s
|
104
|
+
self.integer_value_uncertainty = @value.second
|
105
|
+
else
|
106
|
+
raise "Unknown Field Type: #{field.field_type.inspect}"
|
107
|
+
end
|
108
|
+
|
109
|
+
puts "persisted value '#{value}'"
|
110
|
+
|
111
|
+
return true # Ensure that if we set a value to false we don't accidentally cancel the save
|
112
|
+
end
|
113
|
+
|
114
|
+
def mark_for_destruction_if_blank
|
115
|
+
# Tell the parent object to delete this tag when saving if it is a nil value
|
116
|
+
# NOTE: Boolean's false value evaluates to blank, but should be interpreted as present
|
117
|
+
@marked_for_destruction = (boolean? ? self.value.nil? : self.value.blank?).presence
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Templatr
|
2
|
+
class Template < ActiveRecord::Base
|
3
|
+
validates :name, :presence => true, :uniqueness => {:case_sensitive => false}
|
4
|
+
validate :unique_field_names
|
5
|
+
|
6
|
+
after_validation :add_field_uniqueness_errors
|
7
|
+
|
8
|
+
def self.templatable_class
|
9
|
+
self.to_s[/[A-Z][a-z]+/].constantize
|
10
|
+
end
|
11
|
+
|
12
|
+
# Combined common and default fields
|
13
|
+
def template_fields
|
14
|
+
common_fields + default_fields
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
self.name
|
19
|
+
end
|
20
|
+
|
21
|
+
# In order to make common fields appear on a new form, we need to make the has_many association think that it should load them from the database
|
22
|
+
# We do so by pretending we have a primary key, knowing that it will evaluate to null
|
23
|
+
def attribute_present?(attribute)
|
24
|
+
attribute.to_s == 'common_fields_fake_foreign_key' ? true : super
|
25
|
+
end
|
26
|
+
|
27
|
+
# Ensure all nested attributes for common fields get saved as common fields, and not as template fields
|
28
|
+
def common_fields_attributes=(nested_attributes)
|
29
|
+
nested_attributes.values.each do |attributes|
|
30
|
+
common_field = common_fields.find {|field| field.id.to_s == attributes[:id] && attributes[:id].present? } || common_fields.build
|
31
|
+
assign_to_or_mark_for_destruction(common_field, attributes, true, {})
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def unique_field_names
|
38
|
+
names = template_fields.reject(&:marked_for_destruction?).collect {|f| f.name.downcase }
|
39
|
+
|
40
|
+
errors.add(:base, "fields aren't unique") if names.uniq!
|
41
|
+
end
|
42
|
+
|
43
|
+
# This needs to run after validation because we don't want the child models to clear these errors when they validate
|
44
|
+
def add_field_uniqueness_errors
|
45
|
+
names = template_fields.reject(&:marked_for_destruction?).collect {|f| f.name.downcase }
|
46
|
+
|
47
|
+
template_fields.each do |field|
|
48
|
+
if names.count(field.name.downcase) > 1
|
49
|
+
field.errors.add(:name, "has already been taken")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Templatr</title>
|
5
|
+
<%= stylesheet_link_tag "templatr/application", media: "all" %>
|
6
|
+
<%= javascript_include_tag "templatr/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
module Templatr
|
2
|
+
module ActsAsTemplatable
|
3
|
+
module ActMethod
|
4
|
+
def acts_as_templatable(options = {})
|
5
|
+
extend Templatr::ActsAsTemplatable::ClassMethods
|
6
|
+
include Templatr::ActsAsTemplatable::InstanceMethods
|
7
|
+
|
8
|
+
Templatr::ActsAsTemplatable::HelperMethods.create_field_class(self)
|
9
|
+
Templatr::ActsAsTemplatable::HelperMethods.create_tag_class(self)
|
10
|
+
Templatr::ActsAsTemplatable::HelperMethods.create_template_class(self)
|
11
|
+
|
12
|
+
TagFieldValue.belongs_to tag_class(false).underscore.to_sym, :foreign_key => :tag_id
|
13
|
+
|
14
|
+
FieldValue.has_many tag_class(false).tableize.to_sym, :dependent => :destroy # Destroy all single select tags with this field value
|
15
|
+
FieldValue.has_many :"single_value_#{name.tableize}", :source => name.underscore.to_sym, :through => tag_class(false).tableize.to_sym
|
16
|
+
|
17
|
+
FieldValue.has_many :"multi_value_#{tag_class(false).tableize}", :source => tag_class(false).underscore.to_sym, :through => :tag_field_values, :dependent => :destroy # Destroy all multi select tags with this field value
|
18
|
+
FieldValue.has_many :"multi_value_#{name.tableize}", :source => name.underscore.to_sym, :through => :"multi_value_#{tag_class(false).tableize}"
|
19
|
+
|
20
|
+
FieldValue.send(:define_method, name.tableize) do
|
21
|
+
if field.select_one?
|
22
|
+
single_value_items
|
23
|
+
elsif field.select_multiple?
|
24
|
+
multi_value_items
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class_eval do
|
29
|
+
belongs_to :template, :class_name => template_class
|
30
|
+
delegate :template_fields, :to => :template
|
31
|
+
|
32
|
+
has_many :tags, :class_name => tag_class, :foreign_key => :taggable_id, :order => 'templatr_tags.name ASC', :dependent => :destroy
|
33
|
+
accepts_nested_attributes_for :tags, :allow_destroy => true
|
34
|
+
|
35
|
+
class_attribute :dynamic_facets
|
36
|
+
self.dynamic_facets = []
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
def template_class(constantize = true)
|
43
|
+
klass = "#{self}Template"
|
44
|
+
constantize ? klass.constantize : klass
|
45
|
+
end
|
46
|
+
|
47
|
+
def field_class(constantize = true)
|
48
|
+
klass = "#{self}Field"
|
49
|
+
constantize ? klass.constantize : klass
|
50
|
+
end
|
51
|
+
|
52
|
+
def tag_class(constantize = true)
|
53
|
+
klass = "#{self}Tag"
|
54
|
+
constantize ? klass.constantize : klass
|
55
|
+
end
|
56
|
+
|
57
|
+
def search_class(constantize = true)
|
58
|
+
klass = "#{self}Search"
|
59
|
+
constantize ? klass.constantize : klass
|
60
|
+
end
|
61
|
+
|
62
|
+
def update_dynamic_facets
|
63
|
+
# Load the dynamic fields
|
64
|
+
current_dynamic_facets = []
|
65
|
+
|
66
|
+
field_class.find_each do |field|
|
67
|
+
current_dynamic_facets << field.facet_name
|
68
|
+
|
69
|
+
define_method field.facet_name do
|
70
|
+
tags.detect {|t| t.field_id == field.id }.try(:value) # Detect instead of SQL constrain so we can eager load the tags association
|
71
|
+
end unless field.facet_name.in?(dynamic_facets)
|
72
|
+
|
73
|
+
has_facet field.facet_name, :attribute_type => field.attribute_type, :multiple => field.select_multiple?, :param => field.param
|
74
|
+
end
|
75
|
+
|
76
|
+
# Disable all facets that no longer exist
|
77
|
+
(dynamic_facets - current_dynamic_facets).each {|facet_name| search_class.disable_facet(facet_name) }
|
78
|
+
|
79
|
+
self.dynamic_facets = current_dynamic_facets
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
module InstanceMethods
|
84
|
+
# Returns true if the record is still able to choose which template to use
|
85
|
+
def can_change_template?
|
86
|
+
!persisted? || !template.present?
|
87
|
+
end
|
88
|
+
|
89
|
+
def template_tags(options = {})
|
90
|
+
existing_tags = tags.joins(:field).reorder('templatr_fields.field_group_id, templatr_fields.order')
|
91
|
+
|
92
|
+
return existing_tags unless options[:include_blank]
|
93
|
+
|
94
|
+
# Add non-populated tags so that they show in the form
|
95
|
+
template_fields.collect do |field|
|
96
|
+
existing_tags.detect {|tag| tag.field == field } || Tag.new(:field => field)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def additional_tags
|
101
|
+
tags.where("field_id IS NULL")
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
module HelperMethods
|
106
|
+
def self.create_field_class(templatable_class)
|
107
|
+
field_class = create_class(templatable_class.field_class(false), 'Templatr::Field')
|
108
|
+
|
109
|
+
field_class.belongs_to :template, :class_name => templatable_class.template_class(false), :foreign_key => :template_id, :inverse_of => :default_fields
|
110
|
+
|
111
|
+
field_class.has_many :tags, :class_name => templatable_class.tag_class(false), :foreign_key => :field_id, :dependent => :destroy, :inverse_of => :field
|
112
|
+
field_class.has_many templatable_class.tag_class(false).tableize.to_sym, :through => :tags
|
113
|
+
|
114
|
+
return field_class
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.create_tag_class(templatable_class)
|
118
|
+
tag_class = create_class(templatable_class.tag_class(false), 'Templatr::Tag')
|
119
|
+
|
120
|
+
tag_class.belongs_to templatable_class.to_s.underscore.to_sym, :foreign_key => :taggable_id
|
121
|
+
|
122
|
+
return tag_class
|
123
|
+
end
|
124
|
+
|
125
|
+
def self.create_template_class(templatable_class)
|
126
|
+
template_class = create_class(templatable_class.template_class(false), 'Templatr::Template')
|
127
|
+
|
128
|
+
template_class.has_many :items, :foreign_key => :template_id, :dependent => :destroy
|
129
|
+
template_class.has_many :default_fields, :class_name => templatable_class.field_class(false), :foreign_key => :template_id, :order => 'templatr_fields.field_group_id, templatr_fields.order, templatr_fields.id', :dependent => :destroy, :inverse_of => :template
|
130
|
+
template_class.has_many :common_fields, :class_name => templatable_class.field_class(false), :foreign_key => :template_id, :order => 'templatr_fields.field_group_id, templatr_fields.order, templatr_fields.id', :primary_key => 'common_fields_fake_foreign_key'
|
131
|
+
|
132
|
+
template_class.accepts_nested_attributes_for :default_fields, :common_fields, :allow_destroy => true
|
133
|
+
|
134
|
+
return template_class
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.create_class(klass_name, parent_klass)
|
138
|
+
class_header = "class ::"
|
139
|
+
class_header << klass_name
|
140
|
+
class_header << " < #{parent_klass}" if parent_klass
|
141
|
+
|
142
|
+
begin
|
143
|
+
klass_name.constantize
|
144
|
+
puts "#{klass_name} has already been created"
|
145
|
+
rescue => e
|
146
|
+
puts "Creating class #{klass_name}"
|
147
|
+
eval "#{class_header}; end"
|
148
|
+
end
|
149
|
+
|
150
|
+
return klass_name.constantize
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
data/lib/templatr.rb
ADDED