govuk_content_models 7.1.0 → 7.1.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.
- data/app/models/specialist_document_edition.rb +288 -1
- data/app/models/tag.rb +5 -0
- data/app/traits/taggable.rb +5 -0
- data/lib/govuk_content_models/version.rb +1 -1
- data/test/models/artefact_action_test.rb +1 -0
- data/test/models/artefact_tag_test.rb +25 -5
- data/test/models/tag_test.rb +12 -0
- metadata +4 -4
|
@@ -1,4 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
require "workflow"
|
|
2
|
+
require "fact_check_address"
|
|
3
|
+
|
|
4
|
+
class SpecialistDocumentEdition
|
|
5
|
+
include Mongoid::Document
|
|
6
|
+
include Mongoid::Timestamps
|
|
7
|
+
include Workflow
|
|
8
|
+
|
|
9
|
+
field :panopticon_id, type: String
|
|
10
|
+
field :version_number, type: Integer, default: 1
|
|
11
|
+
field :sibling_in_progress, type: Integer, default: nil
|
|
12
|
+
field :business_proposition, type: Boolean, default: false
|
|
13
|
+
|
|
14
|
+
field :title, type: String
|
|
15
|
+
field :created_at, type: DateTime, default: lambda { Time.zone.now }
|
|
16
|
+
field :overview, type: String
|
|
17
|
+
field :alternative_title, type: String
|
|
18
|
+
field :slug, type: String
|
|
19
|
+
field :section, type: String
|
|
20
|
+
field :department, type: String
|
|
21
|
+
field :rejected_count, type: Integer, default: 0
|
|
22
|
+
field :tags, type: String
|
|
23
|
+
|
|
24
|
+
field :assignee, type: String
|
|
25
|
+
field :creator, type: String
|
|
26
|
+
field :publisher, type: String
|
|
27
|
+
field :archiver, type: String
|
|
28
|
+
|
|
2
29
|
field :summary, type: String
|
|
3
30
|
field :body, type: String
|
|
4
31
|
field :opened_date, type: Date
|
|
@@ -13,4 +40,264 @@ class SpecialistDocumentEdition < Edition
|
|
|
13
40
|
def whole_body
|
|
14
41
|
self.body
|
|
15
42
|
end
|
|
43
|
+
|
|
44
|
+
belongs_to :assigned_to, class_name: "User"
|
|
45
|
+
|
|
46
|
+
scope :lined_up, where(state: "lined_up")
|
|
47
|
+
scope :draft, where(state: "draft")
|
|
48
|
+
scope :amends_needed, where(state: "amends_needed")
|
|
49
|
+
scope :in_review, where(state: "in_review")
|
|
50
|
+
scope :fact_check, where(state: "fact_check")
|
|
51
|
+
scope :fact_check_received, where(state: "fact_check_received")
|
|
52
|
+
scope :ready, where(state: "ready")
|
|
53
|
+
scope :published, where(state: "published")
|
|
54
|
+
scope :archived, where(state: "archived")
|
|
55
|
+
scope :in_progress, where(:state.nin => ["archived", "published"])
|
|
56
|
+
scope :assigned_to, lambda { |user|
|
|
57
|
+
if user
|
|
58
|
+
where(assigned_to_id: user.id)
|
|
59
|
+
else
|
|
60
|
+
where(:assigned_to_id.exists => false)
|
|
61
|
+
end
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
validates :title, presence: true
|
|
65
|
+
validates :version_number, presence: true
|
|
66
|
+
validates :panopticon_id, presence: true
|
|
67
|
+
validates_with SafeHtml
|
|
68
|
+
|
|
69
|
+
before_save :check_for_archived_artefact
|
|
70
|
+
before_destroy :destroy_artefact
|
|
71
|
+
|
|
72
|
+
index "assigned_to_id"
|
|
73
|
+
index "panopticon_id"
|
|
74
|
+
index "state"
|
|
75
|
+
|
|
76
|
+
class << self; attr_accessor :fields_to_clone end
|
|
77
|
+
@fields_to_clone = []
|
|
78
|
+
|
|
79
|
+
alias_method :admin_list_title, :title
|
|
80
|
+
|
|
81
|
+
def series
|
|
82
|
+
Edition.where(panopticon_id: panopticon_id)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def history
|
|
86
|
+
series.order([:version_number, :desc])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def siblings
|
|
90
|
+
series.excludes(id: id)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def previous_siblings
|
|
94
|
+
siblings.where(:version_number.lt => version_number)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def subsequent_siblings
|
|
98
|
+
siblings.where(:version_number.gt => version_number)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def latest_edition?
|
|
102
|
+
subsequent_siblings.empty?
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def published_edition
|
|
106
|
+
series.where(state: "published").order(version_number: "desc").first
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def previous_published_edition
|
|
110
|
+
series.where(state: "published").order(version_number: "desc").second
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def in_progress_sibling
|
|
114
|
+
subsequent_siblings.in_progress.order(version_number: "desc").first
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def can_create_new_edition?
|
|
118
|
+
subsequent_siblings.in_progress.empty?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def meta_data
|
|
122
|
+
PublicationMetadata.new self
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def fact_check_email_address
|
|
126
|
+
FactCheckAddress.new.for_edition(self)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def get_next_version_number
|
|
130
|
+
latest_version = series.order(version_number: "desc").first.version_number
|
|
131
|
+
latest_version + 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def indexable_content
|
|
135
|
+
respond_to?(:parts) ? indexable_content_with_parts : indexable_content_without_parts
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def indexable_content_without_parts
|
|
139
|
+
if respond_to?(:body)
|
|
140
|
+
"#{alternative_title} #{Govspeak::Document.new(body).to_text}".strip
|
|
141
|
+
else
|
|
142
|
+
alternative_title
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def indexable_content_with_parts
|
|
147
|
+
content = indexable_content_without_parts
|
|
148
|
+
return content unless published_edition
|
|
149
|
+
parts.inject([content]) { |acc, part|
|
|
150
|
+
acc.concat([part.title, Govspeak::Document.new(part.body).to_text])
|
|
151
|
+
}.compact.join(" ").strip
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# If the new clone is of the same type, we can copy all its fields over; if
|
|
155
|
+
# we are changing the type of the edition, any fields other than the base
|
|
156
|
+
# fields will likely be meaningless.
|
|
157
|
+
def fields_to_copy(edition_class)
|
|
158
|
+
edition_class == self.class ? self.class.fields_to_clone : []
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def build_clone(edition_class=nil)
|
|
162
|
+
unless state == "published"
|
|
163
|
+
raise "Cloning of non published edition not allowed"
|
|
164
|
+
end
|
|
165
|
+
unless can_create_new_edition?
|
|
166
|
+
raise "Cloning of a published edition when an in-progress edition exists
|
|
167
|
+
is not allowed"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
edition_class = self.class unless edition_class
|
|
171
|
+
new_edition = edition_class.new(title: self.title,
|
|
172
|
+
version_number: get_next_version_number)
|
|
173
|
+
|
|
174
|
+
real_fields_to_merge = fields_to_copy(edition_class) +
|
|
175
|
+
[:panopticon_id, :overview, :alternative_title,
|
|
176
|
+
:slug, :section, :department]
|
|
177
|
+
|
|
178
|
+
real_fields_to_merge.each do |attr|
|
|
179
|
+
new_edition[attr] = read_attribute(attr)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if edition_class == AnswerEdition and %w(GuideEdition ProgrammeEdition TransactionEdition).include?(self.class.name)
|
|
183
|
+
new_edition.body = whole_body
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
if edition_class == TransactionEdition and %w(AnswerEdition GuideEdition ProgrammeEdition).include?(self.class.name)
|
|
187
|
+
new_edition.more_information = whole_body
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
if edition_class == GuideEdition and self.is_a?(AnswerEdition)
|
|
191
|
+
new_edition.parts.build(title: "Part One", body: whole_body,
|
|
192
|
+
slug: "part-one")
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
new_edition
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.find_or_create_from_panopticon_data(panopticon_id,
|
|
199
|
+
importing_user, api_credentials)
|
|
200
|
+
existing_publication = Edition.where(panopticon_id: panopticon_id)
|
|
201
|
+
.order_by([:version_number, :desc]).first
|
|
202
|
+
return existing_publication if existing_publication
|
|
203
|
+
|
|
204
|
+
raise "Artefact not found" unless metadata = Artefact.find(panopticon_id)
|
|
205
|
+
|
|
206
|
+
importing_user.create_edition(metadata.kind.to_sym,
|
|
207
|
+
panopticon_id: metadata.id,
|
|
208
|
+
slug: metadata.slug,
|
|
209
|
+
title: metadata.name,
|
|
210
|
+
section: metadata.section,
|
|
211
|
+
department: metadata.department,
|
|
212
|
+
business_proposition: metadata.business_proposition)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.find_and_identify(slug, edition)
|
|
216
|
+
scope = where(slug: slug)
|
|
217
|
+
|
|
218
|
+
if edition.present? and edition == "latest"
|
|
219
|
+
scope.order_by(:version_number).last
|
|
220
|
+
elsif edition.present?
|
|
221
|
+
scope.where(version_number: edition).first
|
|
222
|
+
else
|
|
223
|
+
scope.where(state: "published").order_by(:created_at).last
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def panopticon_uri
|
|
228
|
+
Plek.current.find("panopticon") + "/artefacts/" + (panopticon_id || slug).to_s
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def format
|
|
232
|
+
self.class.to_s.gsub("Edition", "")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def format_name
|
|
236
|
+
format
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def has_video?
|
|
240
|
+
false
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def safe_to_preview?
|
|
244
|
+
true
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def has_sibling_in_progress?
|
|
248
|
+
! sibling_in_progress.nil?
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Stop broadcasting a delete message unless there are no siblings.
|
|
252
|
+
def broadcast_action(callback_action)
|
|
253
|
+
unless callback_action == "destroyed" and self.siblings.any?
|
|
254
|
+
super(callback_action)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def was_published
|
|
259
|
+
previous_siblings.all.each(&:archive)
|
|
260
|
+
notify_siblings_of_published_edition
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def update_from_artefact(artefact)
|
|
264
|
+
self.title = artefact.name unless published?
|
|
265
|
+
self.slug = artefact.slug
|
|
266
|
+
self.section = artefact.section
|
|
267
|
+
self.department = artefact.department
|
|
268
|
+
self.business_proposition = artefact.business_proposition
|
|
269
|
+
self.save!
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def check_for_archived_artefact
|
|
273
|
+
if panopticon_id
|
|
274
|
+
a = Artefact.find(panopticon_id)
|
|
275
|
+
if a.state == "archived" and changed_attributes.any?
|
|
276
|
+
# If we're only changing the state to archived, that's ok
|
|
277
|
+
# Any other changes are not allowed
|
|
278
|
+
allowed_keys = ["state", "updated_at"]
|
|
279
|
+
unless ((changed_attributes.keys - allowed_keys).empty?) and state == "archived"
|
|
280
|
+
raise "Editing of an edition with an Archived artefact is not allowed"
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def artefact
|
|
287
|
+
Artefact.find(panopticon_id)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# When we delete an edition is the only one in its series
|
|
291
|
+
# we delete the associated artefact to remove all trace of the
|
|
292
|
+
# item from the system.
|
|
293
|
+
#
|
|
294
|
+
# We don't do this by notifying panopticon as this will only ever
|
|
295
|
+
# happen for artefacts representing editions that haven't been
|
|
296
|
+
# published (and therefore aren't registered in the rest of the)
|
|
297
|
+
# system.
|
|
298
|
+
def destroy_artefact
|
|
299
|
+
if can_destroy? && siblings.empty?
|
|
300
|
+
Artefact.find(self.panopticon_id).destroy
|
|
301
|
+
end
|
|
302
|
+
end
|
|
16
303
|
end
|
data/app/models/tag.rb
CHANGED
|
@@ -72,6 +72,11 @@ class Tag
|
|
|
72
72
|
tag_id_list.map { |tag_id| tags.find { |t| t.tag_id == tag_id } }
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def self.by_tag_types_and_ids(tag_types_and_ids)
|
|
76
|
+
list = tag_types_and_ids.map {|hash| hash.slice(:tag_id, :tag_type) }
|
|
77
|
+
any_of(list)
|
|
78
|
+
end
|
|
79
|
+
|
|
75
80
|
# Retrieve a list of tags by tag ID. Any missing tags raise an exception.
|
|
76
81
|
def self.by_tag_ids!(tag_id_list, tag_type = nil)
|
|
77
82
|
tags = by_tag_ids(tag_id_list, tag_type)
|
data/app/traits/taggable.rb
CHANGED
|
@@ -42,6 +42,8 @@ module Taggable
|
|
|
42
42
|
def self.included(klass)
|
|
43
43
|
klass.extend ClassMethods
|
|
44
44
|
klass.field :tag_ids, type: Array, default: []
|
|
45
|
+
klass.field :tags, type: Array, default: []
|
|
46
|
+
|
|
45
47
|
klass.index :tag_ids
|
|
46
48
|
klass.attr_protected :tags, :tag_ids
|
|
47
49
|
klass.__send__ :private, :tag_ids=
|
|
@@ -90,6 +92,9 @@ module Taggable
|
|
|
90
92
|
tags
|
|
91
93
|
|
|
92
94
|
self.tag_ids = @tags.collect(&:tag_id)
|
|
95
|
+
self.tags = @tags.map {|tag|
|
|
96
|
+
{ tag_id: tag.tag_id, tag_type: tag.tag_type }
|
|
97
|
+
}
|
|
93
98
|
end
|
|
94
99
|
|
|
95
100
|
def tags
|
|
@@ -6,6 +6,9 @@ class ArtefactTagTest < ActiveSupport::TestCase
|
|
|
6
6
|
['crime', 'Crime'], ['crime/the-police', 'The Police'], ['crime/batman', 'Batman']
|
|
7
7
|
]
|
|
8
8
|
TEST_KEYWORDS = [['cheese', 'Cheese'], ['bacon', 'Bacon']]
|
|
9
|
+
TEST_LEGACY_SOURCES = [
|
|
10
|
+
['businesslink', 'Business Link'], ['directgov', 'Directgov'], ['dvla', 'DVLA']
|
|
11
|
+
]
|
|
9
12
|
|
|
10
13
|
setup do
|
|
11
14
|
TEST_SECTIONS.each do |tag_id, title|
|
|
@@ -14,6 +17,9 @@ class ArtefactTagTest < ActiveSupport::TestCase
|
|
|
14
17
|
TEST_KEYWORDS.each do |tag_id, title|
|
|
15
18
|
FactoryGirl.create(:tag, :tag_id => tag_id, :tag_type => 'keyword', :title => title)
|
|
16
19
|
end
|
|
20
|
+
TEST_LEGACY_SOURCES.each do |tag_id, title|
|
|
21
|
+
FactoryGirl.create(:tag, :tag_id => tag_id, :tag_type => 'legacy_source', :title => title)
|
|
22
|
+
end
|
|
17
23
|
end
|
|
18
24
|
|
|
19
25
|
test "return primary section title when asked for its section" do
|
|
@@ -37,16 +43,30 @@ class ArtefactTagTest < ActiveSupport::TestCase
|
|
|
37
43
|
assert_equal "#{parent.title}:#{child.title}", a.section
|
|
38
44
|
end
|
|
39
45
|
|
|
40
|
-
test "
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
test "stores the tag type and tag id for each tag" do
|
|
47
|
+
a = FactoryGirl.create(:artefact)
|
|
48
|
+
|
|
49
|
+
a.sections = ['crime', 'crime/the-police']
|
|
50
|
+
a.legacy_sources = ['businesslink']
|
|
51
|
+
a.keywords = ['bacon']
|
|
52
|
+
a.reconcile_tag_ids
|
|
44
53
|
|
|
54
|
+
expected_tags = [
|
|
55
|
+
{ tag_id: "crime", tag_type: "section" },
|
|
56
|
+
{ tag_id: "crime/the-police", tag_type: "section" },
|
|
57
|
+
{ tag_id: "businesslink", tag_type: "legacy_source" },
|
|
58
|
+
{ tag_id: "bacon", tag_type: "keyword" },
|
|
59
|
+
]
|
|
60
|
+
assert_equal ["crime", "crime/the-police", "businesslink", "bacon"], a.tag_ids
|
|
61
|
+
assert_equal expected_tags, a.attributes["tags"]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
test "has legacy_sources tag collection" do
|
|
45
65
|
a = FactoryGirl.build(:artefact)
|
|
46
66
|
a.legacy_sources = ['businesslink', 'dvla']
|
|
47
67
|
a.save
|
|
48
68
|
|
|
49
69
|
a = Artefact.first
|
|
50
|
-
assert_equal [
|
|
70
|
+
assert_equal ["businesslink", "dvla"], a.legacy_source_ids
|
|
51
71
|
end
|
|
52
72
|
end
|
data/test/models/tag_test.rb
CHANGED
|
@@ -89,4 +89,16 @@ class TagTest < ActiveSupport::TestCase
|
|
|
89
89
|
Tag.by_tag_ids!(%w(crime business pie chips), "section")
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
|
+
|
|
93
|
+
test "should return tags given a list of tag ids and tag types" do
|
|
94
|
+
tag_types_and_ids = [
|
|
95
|
+
{ tag_type: "section", tag_id: "crime" },
|
|
96
|
+
{ tag_type: "section", tag_id: "business" },
|
|
97
|
+
{ tag_type: "keyword", tag_id: "pie" },
|
|
98
|
+
{ tag_type: "keyword", tag_id: "chips" }
|
|
99
|
+
]
|
|
100
|
+
tags = Tag.by_tag_types_and_ids(tag_types_and_ids)
|
|
101
|
+
|
|
102
|
+
assert_equal %w{Business Chips Crime Pie}, tags.map(&:title).sort
|
|
103
|
+
end
|
|
92
104
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: govuk_content_models
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.1
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2014-02-
|
|
12
|
+
date: 2014-02-18 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: bson_ext
|
|
@@ -456,7 +456,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
456
456
|
version: '0'
|
|
457
457
|
segments:
|
|
458
458
|
- 0
|
|
459
|
-
hash:
|
|
459
|
+
hash: -3334293540953649833
|
|
460
460
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
461
461
|
none: false
|
|
462
462
|
requirements:
|
|
@@ -465,7 +465,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
465
465
|
version: '0'
|
|
466
466
|
segments:
|
|
467
467
|
- 0
|
|
468
|
-
hash:
|
|
468
|
+
hash: -3334293540953649833
|
|
469
469
|
requirements: []
|
|
470
470
|
rubyforge_project:
|
|
471
471
|
rubygems_version: 1.8.23
|