govuk_content_models 6.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. data/.gitignore +17 -0
  2. data/.ruby-version +1 -0
  3. data/.travis.yml +14 -0
  4. data/CONTRIBUTING.md +22 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +20 -0
  7. data/README.md +5 -0
  8. data/Rakefile +46 -0
  9. data/app/models/action.rb +60 -0
  10. data/app/models/answer_edition.rb +13 -0
  11. data/app/models/artefact.rb +341 -0
  12. data/app/models/artefact_action.rb +27 -0
  13. data/app/models/artefact_external_link.rb +15 -0
  14. data/app/models/business_support/business_size.rb +14 -0
  15. data/app/models/business_support/business_type.rb +14 -0
  16. data/app/models/business_support/location.rb +14 -0
  17. data/app/models/business_support/purpose.rb +14 -0
  18. data/app/models/business_support/sector.rb +14 -0
  19. data/app/models/business_support/stage.rb +14 -0
  20. data/app/models/business_support/support_type.rb +14 -0
  21. data/app/models/business_support_edition.rb +69 -0
  22. data/app/models/campaign_edition.rb +72 -0
  23. data/app/models/completed_transaction_edition.rb +14 -0
  24. data/app/models/curated_list.rb +32 -0
  25. data/app/models/edition.rb +286 -0
  26. data/app/models/expectant.rb +21 -0
  27. data/app/models/expectation.rb +12 -0
  28. data/app/models/guide_edition.rb +19 -0
  29. data/app/models/help_page_edition.rb +13 -0
  30. data/app/models/licence_edition.rb +35 -0
  31. data/app/models/local_authority.rb +58 -0
  32. data/app/models/local_interaction.rb +20 -0
  33. data/app/models/local_service.rb +49 -0
  34. data/app/models/local_transaction_edition.rb +49 -0
  35. data/app/models/overview_dashboard.rb +25 -0
  36. data/app/models/part.rb +28 -0
  37. data/app/models/parted.rb +32 -0
  38. data/app/models/place_edition.rb +20 -0
  39. data/app/models/programme_edition.rb +26 -0
  40. data/app/models/simple_smart_answer_edition.rb +66 -0
  41. data/app/models/simple_smart_answer_edition/node.rb +40 -0
  42. data/app/models/simple_smart_answer_edition/node/option.rb +31 -0
  43. data/app/models/tag.rb +88 -0
  44. data/app/models/transaction_edition.rb +28 -0
  45. data/app/models/travel_advice_edition.rb +177 -0
  46. data/app/models/user.rb +54 -0
  47. data/app/models/video_edition.rb +24 -0
  48. data/app/models/workflow.rb +217 -0
  49. data/app/models/workflow_actor.rb +141 -0
  50. data/app/traits/attachable.rb +60 -0
  51. data/app/traits/govspeak_smart_quotes_fixer.rb +19 -0
  52. data/app/traits/taggable.rb +113 -0
  53. data/app/validators/safe_html.rb +33 -0
  54. data/app/validators/slug_validator.rb +53 -0
  55. data/config/mongoid.yml +5 -0
  56. data/govuk_content_models.gemspec +42 -0
  57. data/jenkins.sh +7 -0
  58. data/lib/fact_check_address.rb +36 -0
  59. data/lib/govuk_content_models.rb +12 -0
  60. data/lib/govuk_content_models/require_all.rb +14 -0
  61. data/lib/govuk_content_models/test_helpers/factories.rb +213 -0
  62. data/lib/govuk_content_models/test_helpers/local_services.rb +24 -0
  63. data/lib/govuk_content_models/version.rb +4 -0
  64. data/test/fixtures/contactotron_api_response.json +1 -0
  65. data/test/fixtures/uploads/image.jpg +0 -0
  66. data/test/models/artefact_action_test.rb +123 -0
  67. data/test/models/artefact_external_link_test.rb +32 -0
  68. data/test/models/artefact_tag_test.rb +52 -0
  69. data/test/models/artefact_test.rb +583 -0
  70. data/test/models/business_support/business_size_test.rb +25 -0
  71. data/test/models/business_support/business_type_test.rb +25 -0
  72. data/test/models/business_support/location_test.rb +25 -0
  73. data/test/models/business_support/purpose_test.rb +29 -0
  74. data/test/models/business_support/sector_test.rb +25 -0
  75. data/test/models/business_support/stage_test.rb +25 -0
  76. data/test/models/business_support/support_type_test.rb +25 -0
  77. data/test/models/business_support_edition_test.rb +186 -0
  78. data/test/models/campaign_edition_test.rb +90 -0
  79. data/test/models/curated_list_test.rb +32 -0
  80. data/test/models/edition_test.rb +826 -0
  81. data/test/models/fact_check_address_test.rb +36 -0
  82. data/test/models/help_page_edition_test.rb +38 -0
  83. data/test/models/licence_edition_test.rb +104 -0
  84. data/test/models/local_authority_test.rb +113 -0
  85. data/test/models/local_service_test.rb +199 -0
  86. data/test/models/local_transaction_edition_test.rb +78 -0
  87. data/test/models/overview_dashboard_test.rb +47 -0
  88. data/test/models/simple_smart_answer_edition_test.rb +169 -0
  89. data/test/models/simple_smart_answer_node_test.rb +134 -0
  90. data/test/models/simple_smart_answer_option_test.rb +90 -0
  91. data/test/models/tag_test.rb +92 -0
  92. data/test/models/time_zone_test.rb +48 -0
  93. data/test/models/transaction_edition_test.rb +20 -0
  94. data/test/models/travel_advice_edition_test.rb +480 -0
  95. data/test/models/user_test.rb +114 -0
  96. data/test/models/video_edition_test.rb +64 -0
  97. data/test/models/workflow_actor_test.rb +61 -0
  98. data/test/models/workflow_test.rb +307 -0
  99. data/test/test_helper.rb +47 -0
  100. data/test/traits/attachable_test.rb +143 -0
  101. data/test/traits/taggable_test.rb +114 -0
  102. data/test/validators/safe_html_validator_test.rb +86 -0
  103. data/test/validators/slug_validator_test.rb +42 -0
  104. metadata +511 -0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 1.9.3-p484
data/.travis.yml ADDED
@@ -0,0 +1,14 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3-p484
4
+ env:
5
+ - GOVUK_APP_DOMAIN=dev.gov.uk
6
+ services:
7
+ - mongodb
8
+ script:
9
+ - bundle exec rake
10
+ branches:
11
+ except:
12
+ - release
13
+ notifications:
14
+ email: false
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,22 @@
1
+ ## Git workflow ##
2
+
3
+ - Pull requests must contain a succint, clear summary of what the user need is driving this feature change.
4
+ - Follow our [Git styleguide](https://github.com/alphagov/styleguides/blob/master/git.md)
5
+ - Make a feature branch
6
+ - Ensure your branch contains logical atomic commits before sending a pull request - follow our [Git styleguide](https://github.com/alphagov/styleguides/blob/master/git.md)
7
+ - Pull requests are automatically integration tested, where applicable using [Travis CI](https://travis-ci.org/), which will report back on whether the tests still pass on your branch
8
+ - You *may* rebase your branch after feedback if it's to include relevant updates from the master branch. We prefer a rebase here to a merge commit as we prefer a clean and straight history on master with discrete merge commits for features
9
+
10
+ ## Copy ##
11
+
12
+ - Follow the [style guide](https://www.gov.uk/designprinciples/styleguide)
13
+ - URLs should use hyphens, not underscores
14
+
15
+ ## Code ##
16
+
17
+ - Must be readable with meaningful naming, eg no short hand single character variable names
18
+ - Follow our [Ruby style guide](https://github.com/alphagov/styleguides/blob/master/ruby.md)
19
+
20
+ ## Testing ##
21
+
22
+ Write tests.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+ source 'https://BnrJb6FZyzspBboNJzYZ@gem.fury.io/govuk/'
3
+
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2012 HM Government (Government Digital Service)
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.md ADDED
@@ -0,0 +1,5 @@
1
+ # Gov.uk Content Models
2
+
3
+ A gem containing the shared models for Panopticon and Publisher.
4
+
5
+ This is a continual **work in progress**.
data/Rakefile ADDED
@@ -0,0 +1,46 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $:.unshift lib unless $:.include?(lib)
3
+
4
+ require "rake"
5
+ require "rake/testtask"
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << "test"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ t.verbose = true
11
+ end
12
+
13
+ require "gem_publisher"
14
+ desc "Publish gem to Gemfury"
15
+ task :publish_gem do |t|
16
+ gem = GemPublisher.publish_if_updated("govuk_content_models.gemspec", :rubygems)
17
+ puts "Published #{gem}" if gem
18
+ end
19
+
20
+ task :check_for_bad_time_handling do
21
+ directories = Dir.glob(File.join(File.dirname(__FILE__), '**', '*.rb'))
22
+ matching_files = directories.select do |filename|
23
+ match = false
24
+ File.open(filename, :encoding => 'utf-8') do |file|
25
+ match = file.grep(%r{Time\.(now|utc|parse)}).any?
26
+ end
27
+ match
28
+ end
29
+ if matching_files.any?
30
+ raise <<-MSG
31
+
32
+ Avoid issues with daylight-savings time by always building instances of
33
+ TimeWithZone and not Time. Use methods like:
34
+ Time.zone.now, Time.zone.parse, n.days.ago, m.hours.from_now, etc
35
+
36
+ in preference to methods like:
37
+ Time.now, Time.utc, Time.parse, etc
38
+
39
+ Files that contain bad Time handling:
40
+ #{matching_files.join("\n ")}
41
+
42
+ MSG
43
+ end
44
+ end
45
+
46
+ task :default => [:test, :check_for_bad_time_handling]
@@ -0,0 +1,60 @@
1
+ require "safe_html"
2
+
3
+ class Action
4
+ include Mongoid::Document
5
+
6
+ STATUS_ACTIONS = [
7
+ CREATE = "create",
8
+ START_WORK = "start_work",
9
+ REQUEST_REVIEW = "request_review",
10
+ APPROVE_REVIEW = "approve_review",
11
+ APPROVE_FACT_CHECK = "approve_fact_check",
12
+ REQUEST_AMENDMENTS = "request_amendments",
13
+ SEND_FACT_CHECK = "send_fact_check",
14
+ RECEIVE_FACT_CHECK = "receive_fact_check",
15
+ SKIP_FACT_CHECK = "skip_fact_check",
16
+ PUBLISH = "publish",
17
+ ARCHIVE = "archive",
18
+ NEW_VERSION = "new_version",
19
+ ]
20
+
21
+ NON_STATUS_ACTIONS = [
22
+ NOTE = "note",
23
+ ASSIGN = "assign",
24
+ ]
25
+
26
+ embedded_in :edition
27
+
28
+ belongs_to :recipient, class_name: "User"
29
+ belongs_to :requester, class_name: "User"
30
+
31
+ field :approver_id, type: Integer
32
+ field :approved, type: DateTime
33
+ field :comment, type: String
34
+ field :comment_sanitized, type: Boolean, default: false
35
+ field :diff, type: String
36
+ field :request_type, type: String
37
+ field :email_addresses, type: String
38
+ field :customised_message, type: String
39
+ field :created_at, type: DateTime, default: lambda { Time.zone.now }
40
+
41
+ GOVSPEAK_FIELDS = []
42
+ validates_with SafeHtml
43
+
44
+ def container_class_name(edition)
45
+ edition.container.class.name.underscore.humanize
46
+ end
47
+
48
+ def status_action?
49
+ STATUS_ACTIONS.include?(request_type)
50
+ end
51
+
52
+ def to_s
53
+ request_type.humanize.capitalize
54
+ end
55
+
56
+ def is_fact_check_request?
57
+ # SEND_FACT_CHECK is now a state - in older publications it isn't
58
+ request_type == SEND_FACT_CHECK || request_type == "fact_check_requested"
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ require "edition"
2
+
3
+ class AnswerEdition < Edition
4
+ field :body, type: String
5
+
6
+ GOVSPEAK_FIELDS = Edition::GOVSPEAK_FIELDS + [:body]
7
+
8
+ @fields_to_clone = [:body]
9
+
10
+ def whole_body
11
+ self.body
12
+ end
13
+ end
@@ -0,0 +1,341 @@
1
+ require "slug_validator"
2
+ require "plek"
3
+ require "traits/taggable"
4
+ require "artefact_action" # Require this when running outside Rails
5
+ require "safe_html"
6
+
7
+ class CannotEditSlugIfEverPublished < ActiveModel::Validator
8
+ def validate(record)
9
+ if record.changes.keys.include?("slug") && record.state_was == "live"
10
+ record.errors[:slug] << ("Cannot edit slug for live artefacts")
11
+ end
12
+ end
13
+ end
14
+
15
+ class Artefact
16
+ include Mongoid::Document
17
+ include Mongoid::Timestamps
18
+
19
+ include Taggable
20
+ stores_tags_for :sections, :writing_teams, :propositions,
21
+ :keywords, :legacy_sources
22
+ has_primary_tag_for :section
23
+
24
+ # NOTE: these fields are deprecated, and soon to be replaced with a
25
+ # tag-based implementation
26
+ field "department", type: String
27
+ field "business_proposition", type: Boolean, default: false
28
+
29
+ field "name", type: String
30
+ field "slug", type: String
31
+ field "paths", type: Array, default: []
32
+ field "prefixes", type: Array, default: []
33
+ field "kind", type: String
34
+ field "owning_app", type: String
35
+ field "rendering_app", type: String
36
+ field "active", type: Boolean, default: false
37
+ field "need_id", type: String
38
+ field "fact_checkers", type: String
39
+ field "publication_id", type: String
40
+ field "description", type: String
41
+ field "state", type: String, default: "draft"
42
+ field "specialist_body", type: String
43
+ field "language", type: String, default: "en"
44
+ field "need_extended_font", type: Boolean, default: false
45
+
46
+ index "slug", :unique => true
47
+
48
+ # This index allows the `relatable_artefacts` method to use an index-covered
49
+ # query, so it doesn't have to load each of the artefacts.
50
+ index [[:name, Mongo::ASCENDING],
51
+ [:state, Mongo::ASCENDING],
52
+ [:kind, Mongo::ASCENDING],
53
+ [:_type, Mongo::ASCENDING],
54
+ [:_id, Mongo::ASCENDING]]
55
+
56
+ scope :not_archived, where(:state.nin => ["archived"])
57
+
58
+ GOVSPEAK_FIELDS = []
59
+
60
+ validates_with SafeHtml
61
+
62
+ MAXIMUM_RELATED_ITEMS = 8
63
+
64
+ FORMATS_BY_DEFAULT_OWNING_APP = {
65
+ "publisher" => ["answer",
66
+ "business_support",
67
+ "campaign",
68
+ "completed_transaction",
69
+ "guide",
70
+ "help_page",
71
+ "licence",
72
+ "local_transaction",
73
+ "place",
74
+ "programme",
75
+ "simple_smart_answer",
76
+ "transaction",
77
+ "video"],
78
+ "smartanswers" => ["smart-answer"],
79
+ "custom-application" => ["custom-application"], # In this case the owning_app is overriden. eg calendars, licencefinder
80
+ "travel-advice-publisher" => ["travel-advice"],
81
+ "whitehall" => ["case_study",
82
+ "consultation",
83
+ "detailed_guide",
84
+ "news_article",
85
+ "speech",
86
+ "policy",
87
+ "publication",
88
+ "statistical_data_set",
89
+ "worldwide_priority"]
90
+ }.freeze
91
+
92
+ FORMATS = FORMATS_BY_DEFAULT_OWNING_APP.values.flatten
93
+
94
+ def self.default_app_for_format(format)
95
+ FORMATS_BY_DEFAULT_OWNING_APP.detect { |app, formats| formats.include?(format) }.first
96
+ end
97
+
98
+ KIND_TRANSLATIONS = {
99
+ "standard transaction link" => "transaction",
100
+ "local authority transaction link" => "local_transaction",
101
+ "completed/done transaction" => "completed_transaction",
102
+ "benefit / scheme" => "programme",
103
+ "find my nearest" => "place",
104
+ }.tap { |h| h.default_proc = -> _, k { k } }.freeze
105
+
106
+ has_and_belongs_to_many :related_artefacts, class_name: "Artefact"
107
+ embeds_many :actions, class_name: "ArtefactAction", order: :created_at
108
+
109
+ embeds_many :external_links, class_name: "ArtefactExternalLink"
110
+ accepts_nested_attributes_for :external_links, :allow_destroy => true,
111
+ reject_if: proc { |attrs| attrs["title"].blank? && attrs["url"].blank? }
112
+
113
+ before_validation :normalise, on: :create
114
+ before_create :record_create_action
115
+ before_update :record_update_action
116
+ after_update :update_editions
117
+
118
+ validates :name, presence: true
119
+ validates :slug, presence: true, uniqueness: true, slug: true
120
+ validates :kind, inclusion: { in: lambda { |x| FORMATS } }
121
+ validates :state, inclusion: { in: ["draft", "live", "archived"] }
122
+ validates :owning_app, presence: true
123
+ validates :language, inclusion: { in: ["en", "cy"] }
124
+ validates_with CannotEditSlugIfEverPublished
125
+ validate :validate_prefixes_and_paths
126
+
127
+ def self.in_alphabetical_order
128
+ order_by([[:name, :asc]])
129
+ end
130
+
131
+ def self.find_by_slug(s)
132
+ where(slug: s).first
133
+ end
134
+
135
+ def self.relatable_items
136
+ # Only retrieving the name field, because that's all we use in Panopticon's
137
+ # helper method (the only place we use this), and it means the index can
138
+ # cover the query entirely
139
+ self.in_alphabetical_order
140
+ .where(:kind.ne => "completed_transaction", :state.ne => "archived")
141
+ .only(:name)
142
+ end
143
+
144
+ # The old-style section string identifier, of the form 'Crime:Prisons'
145
+ def section
146
+ return '' unless self.primary_section
147
+ if primary_section.parent
148
+ [primary_section.parent.title, primary_section.title].join ':'
149
+ else
150
+ primary_section.title
151
+ end
152
+ end
153
+
154
+ # Fallback to english if no language is present
155
+ def language
156
+ attributes['language'] || "en"
157
+ end
158
+
159
+ def normalise
160
+ return unless kind.present?
161
+ self.kind = KIND_TRANSLATIONS[kind.to_s.downcase.strip]
162
+ end
163
+
164
+ def admin_url(options = {})
165
+ [ "#{Plek.current.find(owning_app)}/admin/publications/#{id}",
166
+ options.to_query
167
+ ].reject(&:blank?).join("?")
168
+ end
169
+
170
+ # TODO: Replace this nonsense with a proper API layer.
171
+ def as_json(options={})
172
+ super.tap { |hash|
173
+ if hash["tag_ids"]
174
+ hash["tags"] = Tag.by_tag_ids!(hash["tag_ids"]).map(&:as_json)
175
+ else
176
+ hash["tag_ids"] = []
177
+ hash["tags"] = []
178
+ end
179
+
180
+ if self.primary_section
181
+ hash['primary_section'] = self.primary_section.tag_id
182
+ end
183
+
184
+ unless options[:ignore_related_artefacts]
185
+ hash["related_items"] = published_related_artefacts.map do |a|
186
+ {"artefact" => a.as_json(ignore_related_artefacts: true)}
187
+ end
188
+ end
189
+ hash.delete("related_artefacts")
190
+ hash.delete("related_artefact_ids")
191
+ hash["id"] = hash.delete("_id")
192
+
193
+ # Add a section identifier if needed
194
+ hash["section"] ||= section
195
+ }
196
+ end
197
+
198
+ def published_related_artefacts
199
+ related_artefacts.select do |related_artefact|
200
+ if related_artefact.owning_app == "publisher"
201
+ related_artefact.any_editions_published?
202
+ else
203
+ true
204
+ end
205
+ end
206
+ end
207
+
208
+ # Pass in the desired scope, eg self.related_artefacts.live,
209
+ # get back the items in the order they were set in, rather than natural order
210
+ def ordered_related_artefacts(scope_or_array = self.related_artefacts)
211
+ scope_or_array.sort_by { |artefact| related_artefact_ids.index(artefact.id) }
212
+ end
213
+
214
+ def related_artefacts_grouped_by_distance(scope_or_array = self.related_artefacts)
215
+ groups = { "subsection" => [], "section" => [], "other" => [] }
216
+ scoped_artefacts = ordered_related_artefacts(scope_or_array)
217
+
218
+ if primary_tag = self.primary_section
219
+ groups['subsection'] = scoped_artefacts.select {|a| a.tag_ids.include?(primary_tag.tag_id) }
220
+
221
+ if primary_tag.parent_id.present?
222
+ pattern = Regexp.new "^#{Regexp.quote(primary_tag.parent_id)}\/.+"
223
+ groups['section'] = scoped_artefacts.reject {|a| groups['subsection'].include?(a) }.select {|a|
224
+ a.tag_ids.grep(pattern).count > 0
225
+ }
226
+ end
227
+ end
228
+ groups['other'] = scoped_artefacts.reject {|a| (groups['subsection'] + groups['section']).include?(a) }
229
+
230
+ groups
231
+ end
232
+
233
+ def any_editions_published?
234
+ Edition.where(panopticon_id: self.id, state: 'published').any?
235
+ end
236
+
237
+ def any_editions_ever_published?
238
+ Edition.where(panopticon_id: self.id,
239
+ :state.in => ['published', 'archived']).any?
240
+ end
241
+
242
+ def update_editions
243
+ if state != 'archived'
244
+ Edition.where(:state.nin => ["archived"],
245
+ panopticon_id: self.id).each do |edition|
246
+ edition.update_from_artefact(self)
247
+ end
248
+ else
249
+ archive_editions
250
+ end
251
+ end
252
+
253
+ def archive_editions
254
+ if state == 'archived'
255
+ Edition.where(panopticon_id: self.id, :state.nin => ["archived"]).each do |edition|
256
+ edition.new_action(self, "note", comment: "Artefact has been archived. Archiving this edition.")
257
+ edition.archive!
258
+ end
259
+ end
260
+ end
261
+
262
+ def self.from_param(slug_or_id)
263
+ find_by_slug(slug_or_id) || find(slug_or_id)
264
+ rescue BSON::InvalidObjectId
265
+ raise Mongoid::Errors::DocumentNotFound.new(self, slug_or_id)
266
+ end
267
+
268
+ def update_attributes_as(user, *args)
269
+ assign_attributes(*args)
270
+ save_as user
271
+ end
272
+
273
+ def save_as(user, options={})
274
+ default_action = new_record? ? "create" : "update"
275
+ action_type = options.delete(:action_type) || default_action
276
+ record_action action_type, user: user
277
+ save(options)
278
+ end
279
+
280
+ def record_create_action
281
+ record_action "create"
282
+ end
283
+
284
+ def record_update_action
285
+ record_action "update"
286
+ end
287
+
288
+ def record_action(action_type, options={})
289
+ user = options[:user]
290
+ current_snapshot = snapshot
291
+ last_snapshot = actions.last ? actions.last.snapshot : nil
292
+ unless current_snapshot == last_snapshot
293
+ new_action = actions.build(
294
+ user: user,
295
+ action_type: action_type,
296
+ snapshot: current_snapshot
297
+ )
298
+ # Mongoid will not fire creation callbacks on embedded documents, so we
299
+ # need to trigger this manually. There is a `cascade_callbacks` option on
300
+ # `embeds_many`, but it doesn't appear to trigger creation events on
301
+ # children when an update event fires on the parent
302
+ new_action.set_created_at
303
+ end
304
+ end
305
+
306
+ def archived?
307
+ self.state == "archived"
308
+ end
309
+
310
+ def live?
311
+ self.state == "live"
312
+ end
313
+
314
+ def snapshot
315
+ reconcile_tag_ids
316
+ attributes.except "_id", "created_at", "updated_at", "actions"
317
+ end
318
+
319
+ private
320
+
321
+ def validate_prefixes_and_paths
322
+ if ! self.prefixes.nil? and self.prefixes_changed?
323
+ if self.prefixes.any? {|p| ! valid_url_path?(p)}
324
+ errors.add(:prefixes, "are not all valid absolute URL paths")
325
+ end
326
+ end
327
+ if ! self.paths.nil? and self.paths_changed?
328
+ if self.paths.any? {|p| ! valid_url_path?(p)}
329
+ errors.add(:paths, "are not all valid absolute URL paths")
330
+ end
331
+ end
332
+ end
333
+
334
+ def valid_url_path?(path)
335
+ return false unless path.starts_with?("/")
336
+ uri = URI.parse(path)
337
+ uri.path == path && path !~ %r{//} && path !~ %r{./\z}
338
+ rescue URI::InvalidURIError
339
+ false
340
+ end
341
+ end