templatr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +3 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/javascripts/templatr/application.js +13 -0
  6. data/app/assets/stylesheets/templatr/application.css +13 -0
  7. data/app/controllers/templatr/application_controller.rb +4 -0
  8. data/app/helpers/templatr/application_helper.rb +4 -0
  9. data/app/models/templatr/field.rb +168 -0
  10. data/app/models/templatr/field_group.rb +5 -0
  11. data/app/models/templatr/field_value.rb +13 -0
  12. data/app/models/templatr/tag.rb +120 -0
  13. data/app/models/templatr/tag_field_value.rb +6 -0
  14. data/app/models/templatr/template.rb +54 -0
  15. data/app/views/layouts/templatr/application.html.erb +14 -0
  16. data/config/routes.rb +2 -0
  17. data/lib/tasks/templatr_tasks.rake +4 -0
  18. data/lib/templatr/acts_as_templatable.rb +154 -0
  19. data/lib/templatr/engine.rb +11 -0
  20. data/lib/templatr/version.rb +3 -0
  21. data/lib/templatr.rb +4 -0
  22. data/test/dummy/README.rdoc +28 -0
  23. data/test/dummy/Rakefile +6 -0
  24. data/test/dummy/app/assets/javascripts/application.js +13 -0
  25. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  26. data/test/dummy/app/controllers/application_controller.rb +5 -0
  27. data/test/dummy/app/helpers/application_helper.rb +2 -0
  28. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  29. data/test/dummy/bin/bundle +3 -0
  30. data/test/dummy/bin/rails +4 -0
  31. data/test/dummy/bin/rake +4 -0
  32. data/test/dummy/config/application.rb +23 -0
  33. data/test/dummy/config/boot.rb +5 -0
  34. data/test/dummy/config/database.yml +25 -0
  35. data/test/dummy/config/environment.rb +5 -0
  36. data/test/dummy/config/environments/development.rb +29 -0
  37. data/test/dummy/config/environments/production.rb +80 -0
  38. data/test/dummy/config/environments/test.rb +36 -0
  39. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  40. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  41. data/test/dummy/config/initializers/inflections.rb +16 -0
  42. data/test/dummy/config/initializers/mime_types.rb +5 -0
  43. data/test/dummy/config/initializers/secret_token.rb +12 -0
  44. data/test/dummy/config/initializers/session_store.rb +3 -0
  45. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  46. data/test/dummy/config/locales/en.yml +23 -0
  47. data/test/dummy/config/routes.rb +4 -0
  48. data/test/dummy/config.ru +4 -0
  49. data/test/dummy/public/404.html +58 -0
  50. data/test/dummy/public/422.html +58 -0
  51. data/test/dummy/public/500.html +57 -0
  52. data/test/dummy/public/favicon.ico +0 -0
  53. data/test/integration/navigation_test.rb +10 -0
  54. data/test/templatr_test.rb +7 -0
  55. data/test/test_helper.rb +15 -0
  56. 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
@@ -0,0 +1,3 @@
1
+ = Templatr
2
+
3
+ This project rocks and uses MIT-LICENSE.
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,4 @@
1
+ module Templatr
2
+ class ApplicationController < ActionController::Base
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Templatr
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -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,5 @@
1
+ module Templatr
2
+ class FieldGroup < ActiveRecord::Base
3
+ has_many :fields
4
+ end
5
+ 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,6 @@
1
+ module Templatr
2
+ class TagFieldValue < ActiveRecord::Base
3
+ belongs_to :field_value
4
+ belongs_to :tag
5
+ end
6
+ 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,2 @@
1
+ Templatr::Engine.routes.draw do
2
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :templatr do
3
+ # # Task goes here
4
+ # end
@@ -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
@@ -0,0 +1,11 @@
1
+ require 'templatr/acts_as_templatable'
2
+
3
+ module Templatr
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Templatr
6
+
7
+ initializer "templatr.init" do
8
+ ActiveRecord::Base.extend ActsAsTemplatable::ActMethod
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module Templatr
2
+ VERSION = "0.0.1"
3
+ end
data/lib/templatr.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "templatr/engine"
2
+
3
+ module Templatr
4
+ end