templatr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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