govuk_content_models 6.0.2
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.
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +14 -0
- data/CONTRIBUTING.md +22 -0
- data/Gemfile +4 -0
- data/LICENSE +20 -0
- data/README.md +5 -0
- data/Rakefile +46 -0
- data/app/models/action.rb +60 -0
- data/app/models/answer_edition.rb +13 -0
- data/app/models/artefact.rb +341 -0
- data/app/models/artefact_action.rb +27 -0
- data/app/models/artefact_external_link.rb +15 -0
- data/app/models/business_support/business_size.rb +14 -0
- data/app/models/business_support/business_type.rb +14 -0
- data/app/models/business_support/location.rb +14 -0
- data/app/models/business_support/purpose.rb +14 -0
- data/app/models/business_support/sector.rb +14 -0
- data/app/models/business_support/stage.rb +14 -0
- data/app/models/business_support/support_type.rb +14 -0
- data/app/models/business_support_edition.rb +69 -0
- data/app/models/campaign_edition.rb +72 -0
- data/app/models/completed_transaction_edition.rb +14 -0
- data/app/models/curated_list.rb +32 -0
- data/app/models/edition.rb +286 -0
- data/app/models/expectant.rb +21 -0
- data/app/models/expectation.rb +12 -0
- data/app/models/guide_edition.rb +19 -0
- data/app/models/help_page_edition.rb +13 -0
- data/app/models/licence_edition.rb +35 -0
- data/app/models/local_authority.rb +58 -0
- data/app/models/local_interaction.rb +20 -0
- data/app/models/local_service.rb +49 -0
- data/app/models/local_transaction_edition.rb +49 -0
- data/app/models/overview_dashboard.rb +25 -0
- data/app/models/part.rb +28 -0
- data/app/models/parted.rb +32 -0
- data/app/models/place_edition.rb +20 -0
- data/app/models/programme_edition.rb +26 -0
- data/app/models/simple_smart_answer_edition.rb +66 -0
- data/app/models/simple_smart_answer_edition/node.rb +40 -0
- data/app/models/simple_smart_answer_edition/node/option.rb +31 -0
- data/app/models/tag.rb +88 -0
- data/app/models/transaction_edition.rb +28 -0
- data/app/models/travel_advice_edition.rb +177 -0
- data/app/models/user.rb +54 -0
- data/app/models/video_edition.rb +24 -0
- data/app/models/workflow.rb +217 -0
- data/app/models/workflow_actor.rb +141 -0
- data/app/traits/attachable.rb +60 -0
- data/app/traits/govspeak_smart_quotes_fixer.rb +19 -0
- data/app/traits/taggable.rb +113 -0
- data/app/validators/safe_html.rb +33 -0
- data/app/validators/slug_validator.rb +53 -0
- data/config/mongoid.yml +5 -0
- data/govuk_content_models.gemspec +42 -0
- data/jenkins.sh +7 -0
- data/lib/fact_check_address.rb +36 -0
- data/lib/govuk_content_models.rb +12 -0
- data/lib/govuk_content_models/require_all.rb +14 -0
- data/lib/govuk_content_models/test_helpers/factories.rb +213 -0
- data/lib/govuk_content_models/test_helpers/local_services.rb +24 -0
- data/lib/govuk_content_models/version.rb +4 -0
- data/test/fixtures/contactotron_api_response.json +1 -0
- data/test/fixtures/uploads/image.jpg +0 -0
- data/test/models/artefact_action_test.rb +123 -0
- data/test/models/artefact_external_link_test.rb +32 -0
- data/test/models/artefact_tag_test.rb +52 -0
- data/test/models/artefact_test.rb +583 -0
- data/test/models/business_support/business_size_test.rb +25 -0
- data/test/models/business_support/business_type_test.rb +25 -0
- data/test/models/business_support/location_test.rb +25 -0
- data/test/models/business_support/purpose_test.rb +29 -0
- data/test/models/business_support/sector_test.rb +25 -0
- data/test/models/business_support/stage_test.rb +25 -0
- data/test/models/business_support/support_type_test.rb +25 -0
- data/test/models/business_support_edition_test.rb +186 -0
- data/test/models/campaign_edition_test.rb +90 -0
- data/test/models/curated_list_test.rb +32 -0
- data/test/models/edition_test.rb +826 -0
- data/test/models/fact_check_address_test.rb +36 -0
- data/test/models/help_page_edition_test.rb +38 -0
- data/test/models/licence_edition_test.rb +104 -0
- data/test/models/local_authority_test.rb +113 -0
- data/test/models/local_service_test.rb +199 -0
- data/test/models/local_transaction_edition_test.rb +78 -0
- data/test/models/overview_dashboard_test.rb +47 -0
- data/test/models/simple_smart_answer_edition_test.rb +169 -0
- data/test/models/simple_smart_answer_node_test.rb +134 -0
- data/test/models/simple_smart_answer_option_test.rb +90 -0
- data/test/models/tag_test.rb +92 -0
- data/test/models/time_zone_test.rb +48 -0
- data/test/models/transaction_edition_test.rb +20 -0
- data/test/models/travel_advice_edition_test.rb +480 -0
- data/test/models/user_test.rb +114 -0
- data/test/models/video_edition_test.rb +64 -0
- data/test/models/workflow_actor_test.rb +61 -0
- data/test/models/workflow_test.rb +307 -0
- data/test/test_helper.rb +47 -0
- data/test/traits/attachable_test.rb +143 -0
- data/test/traits/taggable_test.rb +114 -0
- data/test/validators/safe_html_validator_test.rb +86 -0
- data/test/validators/slug_validator_test.rb +42 -0
- metadata +511 -0
data/.gitignore
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.9.3-p484
|
data/.travis.yml
ADDED
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
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
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,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
|