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.
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