govuk_content_models 7.1.0 → 7.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|