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
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
require "differ"
|
|
2
|
+
require "state_machine"
|
|
3
|
+
require "action"
|
|
4
|
+
|
|
5
|
+
module Workflow
|
|
6
|
+
class CannotDeletePublishedPublication < RuntimeError; end
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
included do
|
|
10
|
+
validate :not_editing_published_item
|
|
11
|
+
before_destroy :check_can_delete_and_notify
|
|
12
|
+
|
|
13
|
+
before_save :denormalise_users
|
|
14
|
+
after_create :notify_siblings_of_new_edition
|
|
15
|
+
|
|
16
|
+
field :state, type: String, default: "lined_up"
|
|
17
|
+
belongs_to :assigned_to, class_name: "User"
|
|
18
|
+
embeds_many :actions
|
|
19
|
+
|
|
20
|
+
state_machine initial: :lined_up do
|
|
21
|
+
after_transition on: :request_amendments do |edition, transition|
|
|
22
|
+
edition.mark_as_rejected
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
after_transition on: :publish do |edition, transition|
|
|
26
|
+
edition.was_published
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
event :start_work do
|
|
30
|
+
transition lined_up: :draft
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
event :request_review do
|
|
34
|
+
transition [:draft, :amends_needed] => :in_review
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
event :approve_review do
|
|
38
|
+
transition in_review: :ready
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
event :approve_fact_check do
|
|
42
|
+
transition fact_check_received: :ready
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
event :request_amendments do
|
|
46
|
+
transition [:fact_check_received, :in_review] => :amends_needed
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Editions can optionally be sent out for fact check
|
|
50
|
+
event :send_fact_check do
|
|
51
|
+
transition ready: :fact_check
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# If no response is received to a fact check request we can skip
|
|
55
|
+
# that fact check and return the edition to the 'ready' state
|
|
56
|
+
event :skip_fact_check do
|
|
57
|
+
transition fact_check: :ready
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Where a fact check response has been received the item is moved
|
|
61
|
+
# into a special state so that the fact check responses can be
|
|
62
|
+
# reviewed
|
|
63
|
+
event :receive_fact_check do
|
|
64
|
+
transition fact_check: :fact_check_received
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
event :publish do
|
|
68
|
+
transition ready: :published
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
event :emergency_publish do
|
|
72
|
+
transition draft: :published
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
event :archive do
|
|
76
|
+
transition all => :archived, :unless => :archived?
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# alias_method :created_by, :creator
|
|
81
|
+
# alias_method :published_by, :publisher
|
|
82
|
+
# alias_method :archived_by, :archiver
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def fact_checked?
|
|
86
|
+
(self.actions.where(request_type: Action::APPROVE_FACT_CHECK).count > 0)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def capitalized_state_name
|
|
90
|
+
self.human_state_name.capitalize
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def update_user_action(property, statuses)
|
|
94
|
+
actions.where(:request_type.in => statuses).limit(1).each do |action|
|
|
95
|
+
# This can be invoked by Panopticon when it updates an artefact and associated
|
|
96
|
+
# editions. The problem is that Panopticon and Publisher users live in different
|
|
97
|
+
# collections, but share a model and relationships with eg actions.
|
|
98
|
+
# Therefore, Panopticon might not find a user for an action.
|
|
99
|
+
if action.requester
|
|
100
|
+
self[property] = action.requester.name
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def denormalise_users
|
|
106
|
+
self.assignee = assigned_to.name if assigned_to
|
|
107
|
+
update_user_action("creator", [Action::CREATE, Action::NEW_VERSION])
|
|
108
|
+
update_user_action("publisher", [Action::PUBLISH])
|
|
109
|
+
update_user_action("archiver", [Action::ARCHIVE])
|
|
110
|
+
self
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def created_by
|
|
114
|
+
creation = actions.detect do |a|
|
|
115
|
+
a.request_type == Action::CREATE || a.request_type == Action::NEW_VERSION
|
|
116
|
+
end
|
|
117
|
+
creation.requester if creation
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def published_by
|
|
121
|
+
publication = actions.where(request_type: Action::PUBLISH).first
|
|
122
|
+
publication.requester if publication
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def archived_by
|
|
126
|
+
publication = actions.where(request_type: Action::ARCHIVE).first
|
|
127
|
+
publication.requester if publication
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def latest_status_action(type = nil)
|
|
131
|
+
if type
|
|
132
|
+
self.actions.where(request_type: type).last
|
|
133
|
+
else
|
|
134
|
+
most_recent_action(&:status_action?)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def last_fact_checked_at
|
|
139
|
+
last_fact_check = actions.reverse.find(&:is_fact_check_request?)
|
|
140
|
+
last_fact_check ? last_fact_check.created_at : NullTimestamp.new
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def new_action(user, type, options={})
|
|
144
|
+
actions.create!(options.merge(requester_id: user.id, request_type: type))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def new_action_without_validation(user, type, options={})
|
|
148
|
+
action = actions.build(options.merge(requester_id: user.id, request_type: type))
|
|
149
|
+
save(validate: false)
|
|
150
|
+
action
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def most_recent_action(&blk)
|
|
154
|
+
self.actions.sort_by(&:created_at).reverse.find(&blk)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def not_editing_published_item
|
|
158
|
+
if changed? and ! state_changed?
|
|
159
|
+
if archived?
|
|
160
|
+
errors.add(:base, "Archived editions can't be edited")
|
|
161
|
+
end
|
|
162
|
+
if published?
|
|
163
|
+
changes_allowed_when_published = ["slug", "section",
|
|
164
|
+
"department", "business_proposition"]
|
|
165
|
+
illegal_changes = changes.keys - changes_allowed_when_published
|
|
166
|
+
if illegal_changes.empty?
|
|
167
|
+
# Allow it
|
|
168
|
+
else
|
|
169
|
+
errors.add(:base, "Published editions can't be edited")
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def can_destroy?
|
|
176
|
+
! published? and ! archived?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def check_can_delete_and_notify
|
|
180
|
+
raise CannotDeletePublishedPublication unless can_destroy?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def mark_as_rejected
|
|
184
|
+
self.inc(:rejected_count, 1)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def previous_edition
|
|
188
|
+
self.previous_published_edition || false
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def edition_changes
|
|
192
|
+
if self.whole_body.empty?
|
|
193
|
+
false
|
|
194
|
+
else
|
|
195
|
+
my_body, their_body = [self, self.published_edition].map do |edition|
|
|
196
|
+
edition.whole_body.gsub("\r\n", "\n")
|
|
197
|
+
end
|
|
198
|
+
Differ.diff_by_line(my_body, their_body)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def notify_siblings_of_new_edition
|
|
203
|
+
siblings.update_all(sibling_in_progress: self.version_number)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def notify_siblings_of_published_edition
|
|
207
|
+
siblings.update_all(sibling_in_progress: nil)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def update_sibling_in_progress(version_number_or_nil)
|
|
211
|
+
update_attribute(:sibling_in_progress, version_number_or_nil)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def in_progress?
|
|
215
|
+
! ["archived", "published"].include? self.state
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
require "answer_edition"
|
|
2
|
+
require "guide_edition"
|
|
3
|
+
require "local_transaction_edition"
|
|
4
|
+
require "place_edition"
|
|
5
|
+
require "programme_edition"
|
|
6
|
+
require "transaction_edition"
|
|
7
|
+
|
|
8
|
+
module WorkflowActor
|
|
9
|
+
SIMPLE_WORKFLOW_ACTIONS = %W[start_work request_review
|
|
10
|
+
request_amendments approve_review approve_fact_check archive]
|
|
11
|
+
|
|
12
|
+
def record_action(edition, type, options={})
|
|
13
|
+
type = Action.const_get(type.to_s.upcase)
|
|
14
|
+
action = edition.new_action(self, type, options)
|
|
15
|
+
edition.save! # force callbacks for denormalisation
|
|
16
|
+
action
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def record_action_without_validation(edition, type, options={})
|
|
20
|
+
type = Action.const_get(type.to_s.upcase)
|
|
21
|
+
action = edition.new_action_without_validation(self, type, options)
|
|
22
|
+
edition.save! # force callbacks for denormalisation
|
|
23
|
+
action
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def can_take_action(action, edition)
|
|
27
|
+
respond_to?(:"can_#{action}?") ? __send__(:"can_#{action}?", edition) : true
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def take_action(edition, action, details = {})
|
|
31
|
+
if can_take_action(action, edition) and edition.send(action)
|
|
32
|
+
record_action(edition, action, details)
|
|
33
|
+
edition
|
|
34
|
+
else
|
|
35
|
+
false
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def take_action!(edition, action, details = {})
|
|
40
|
+
edition = take_action(edition, action, details)
|
|
41
|
+
edition.save if edition
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def progress(edition, activity_details)
|
|
45
|
+
activity = activity_details.delete(:request_type)
|
|
46
|
+
|
|
47
|
+
edition = send(activity, edition, activity_details)
|
|
48
|
+
edition.save if edition
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def record_note(edition, comment)
|
|
52
|
+
edition.new_action(self, "note", comment: comment)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def create_edition(format, attributes = {})
|
|
56
|
+
format = "#{format}_edition" unless format.to_s.match(/edition$/)
|
|
57
|
+
publication_class = format.to_s.camelize.constantize
|
|
58
|
+
|
|
59
|
+
item = publication_class.create(attributes)
|
|
60
|
+
record_action(item, Action::CREATE) if item.persisted?
|
|
61
|
+
item
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def new_version(edition, convert_to = nil)
|
|
65
|
+
return false unless edition.published?
|
|
66
|
+
|
|
67
|
+
if not convert_to.nil?
|
|
68
|
+
convert_to = convert_to.to_s.camelize.constantize
|
|
69
|
+
new_edition = edition.build_clone(convert_to)
|
|
70
|
+
else
|
|
71
|
+
new_edition = edition.build_clone
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if new_edition
|
|
75
|
+
record_action new_edition, Action::NEW_VERSION
|
|
76
|
+
new_edition
|
|
77
|
+
else
|
|
78
|
+
false
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def send_fact_check(edition, details)
|
|
83
|
+
return false if details[:email_addresses].blank?
|
|
84
|
+
|
|
85
|
+
details[:comment] ||= "Fact check requested"
|
|
86
|
+
details[:comment] += "\n\nResponses should be sent to: " +
|
|
87
|
+
edition.fact_check_email_address
|
|
88
|
+
|
|
89
|
+
take_action(edition, __method__, details)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Advances state if possible (i.e. if in "fact_check" state)
|
|
93
|
+
# Always records the action.
|
|
94
|
+
def receive_fact_check(edition, details)
|
|
95
|
+
edition.receive_fact_check
|
|
96
|
+
# Fact checks are processed async, so the user doesn't get an opportunity
|
|
97
|
+
# to retry without the content that (inadvertantly) fails validation, which happens frequently.
|
|
98
|
+
record_action_without_validation(edition, :receive_fact_check, details)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def skip_fact_check(edition, details)
|
|
102
|
+
edition.skip_fact_check
|
|
103
|
+
record_action(edition, :skip_fact_check, details)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
SIMPLE_WORKFLOW_ACTIONS.each do |method|
|
|
107
|
+
define_method(method) do |edition, details = {}|
|
|
108
|
+
take_action(edition, __method__, details)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def publish(edition, details)
|
|
113
|
+
if edition.published_edition
|
|
114
|
+
details.merge!({ diff: edition.edition_changes })
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
take_action(edition, __method__, details)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def can_approve_review?(edition)
|
|
121
|
+
# To accommodate latest_status_action being nil, we'll always return true in
|
|
122
|
+
# those cases
|
|
123
|
+
# This is intended as a v.temporary fix until we can remedy the root cause
|
|
124
|
+
if edition.latest_status_action
|
|
125
|
+
edition.latest_status_action.requester_id != self.id
|
|
126
|
+
else
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
alias :can_request_amendments? :can_approve_review?
|
|
131
|
+
|
|
132
|
+
def assign(edition, recipient)
|
|
133
|
+
edition.assigned_to_id = recipient.id
|
|
134
|
+
|
|
135
|
+
# We're saving the edition here as the controller treats assignment as a
|
|
136
|
+
# special case.
|
|
137
|
+
# The controller saves the publication, then updates assignment.
|
|
138
|
+
edition.save! and edition.reload
|
|
139
|
+
record_action edition, __method__, recipient: recipient
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Attachable
|
|
2
|
+
class ApiClientNotPresent < StandardError; end
|
|
3
|
+
|
|
4
|
+
@asset_api_client = nil
|
|
5
|
+
|
|
6
|
+
def self.asset_api_client
|
|
7
|
+
@asset_api_client
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.asset_api_client=(api_client)
|
|
11
|
+
@asset_api_client = api_client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module ClassMethods
|
|
15
|
+
def attaches(*fields)
|
|
16
|
+
fields.map(&:to_s).each do |field|
|
|
17
|
+
before_save "upload_#{field}".to_sym, :if => "#{field}_has_changed?".to_sym
|
|
18
|
+
self.field "#{field}_id".to_sym, type: String
|
|
19
|
+
|
|
20
|
+
define_method(field) do
|
|
21
|
+
raise ApiClientNotPresent unless Attachable.asset_api_client
|
|
22
|
+
unless self.send("#{field}_id").nil?
|
|
23
|
+
@attachments ||= { }
|
|
24
|
+
@attachments[field] ||= Attachable.asset_api_client.asset(self.send("#{field}_id"))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
define_method("#{field}=") do |file|
|
|
29
|
+
instance_variable_set("@#{field}_has_changed", true)
|
|
30
|
+
instance_variable_set("@#{field}_file", file)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_method("#{field}_has_changed?") do
|
|
34
|
+
instance_variable_get("@#{field}_has_changed")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
define_method("remove_#{field}=") do |value|
|
|
38
|
+
unless value.nil? or value == false or (value.respond_to?(:empty?) and value.empty?)
|
|
39
|
+
self.send("#{field}_id=", nil)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
define_method("upload_#{field}") do
|
|
44
|
+
raise ApiClientNotPresent unless Attachable.asset_api_client
|
|
45
|
+
begin
|
|
46
|
+
response = Attachable.asset_api_client.create_asset(:file => instance_variable_get("@#{field}_file"))
|
|
47
|
+
self.send("#{field}_id=", response.id.match(/\/([^\/]+)\z/) {|m| m[1] })
|
|
48
|
+
rescue StandardError
|
|
49
|
+
errors.add("#{field}_id".to_sym, "could not be uploaded")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
private "upload_#{field}".to_sym
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def self.included(klass)
|
|
58
|
+
klass.extend ClassMethods
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# encoding: UTF-8
|
|
2
|
+
|
|
3
|
+
module GovspeakSmartQuotesFixer
|
|
4
|
+
def self.included(model)
|
|
5
|
+
model.class_eval do
|
|
6
|
+
before_validation :fix_smart_quotes_in_govspeak
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def fix_smart_quotes_in_govspeak
|
|
13
|
+
self.class::GOVSPEAK_FIELDS.each do |field|
|
|
14
|
+
if self.send(field) =~ /[“”]/
|
|
15
|
+
self.send(field).gsub!(/[“”]/, '"')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
module Taggable
|
|
2
|
+
module ClassMethods
|
|
3
|
+
def stores_tags_for(*keys)
|
|
4
|
+
tag_types = keys.to_a.flatten.compact.map(&:to_s)
|
|
5
|
+
class_attribute :tag_types
|
|
6
|
+
self.tag_types = tag_types
|
|
7
|
+
|
|
8
|
+
tag_types.each do |k|
|
|
9
|
+
define_method "#{k}=" do |values|
|
|
10
|
+
set_tags_of_type(k, values)
|
|
11
|
+
end
|
|
12
|
+
alias_method :"#{k.singularize}_ids=", :"#{k}="
|
|
13
|
+
|
|
14
|
+
define_method k do
|
|
15
|
+
tags_of_type(k.singularize)
|
|
16
|
+
end
|
|
17
|
+
define_method "#{k.singularize}_ids" do
|
|
18
|
+
tags_of_type(k.singularize).collect(&:tag_id)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def has_primary_tag_for(*keys)
|
|
24
|
+
tag_types = keys.to_a.flatten.compact.map(&:to_s)
|
|
25
|
+
class_attribute :primary_tag_types
|
|
26
|
+
self.primary_tag_types = tag_types
|
|
27
|
+
|
|
28
|
+
tag_types.each do |key|
|
|
29
|
+
method_name = "primary_#{key}"
|
|
30
|
+
|
|
31
|
+
define_method "#{method_name}=" do |value|
|
|
32
|
+
set_primary_tag_of_type(key.to_s, value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
define_method method_name do
|
|
36
|
+
tags_of_type(key.to_s).first
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.included(klass)
|
|
43
|
+
klass.extend ClassMethods
|
|
44
|
+
klass.field :tag_ids, type: Array, default: []
|
|
45
|
+
klass.index :tag_ids
|
|
46
|
+
klass.attr_protected :tags, :tag_ids
|
|
47
|
+
klass.__send__ :private, :tag_ids=
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def set_tags_of_type(collection_name, values)
|
|
51
|
+
tag_type = collection_name.singularize
|
|
52
|
+
|
|
53
|
+
# Ensure all tags loaded from database. This feels inelegant
|
|
54
|
+
# but ensures integrity. It could go away if we moved to a more
|
|
55
|
+
# consistent repository approach to retrieving and constructing
|
|
56
|
+
# objects, or if we used a custom serialization type for tags
|
|
57
|
+
# as documented on http://mongoid.org/en/mongoid/docs/documents.html
|
|
58
|
+
tags
|
|
59
|
+
|
|
60
|
+
# This will raise a Tag::MissingTags exception unless all the tags exist
|
|
61
|
+
new_tags = Tag.by_tag_ids!(values, tag_type)
|
|
62
|
+
|
|
63
|
+
@tags.reject! { |t| t.tag_type == tag_type }
|
|
64
|
+
@tags += new_tags
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The primary tag is simply the first one of its
|
|
68
|
+
# type. If that tag is already applied this method
|
|
69
|
+
# moves it to the start of the list. If it's not then
|
|
70
|
+
# we add it at the start of the list.
|
|
71
|
+
def set_primary_tag_of_type(tag_type, value)
|
|
72
|
+
tags
|
|
73
|
+
|
|
74
|
+
tag = Tag.by_tag_id(value, tag_type)
|
|
75
|
+
raise "Missing tag" unless tag
|
|
76
|
+
raise "Wrong tag type" unless tag.tag_type == tag_type
|
|
77
|
+
|
|
78
|
+
@tags -= [tag]
|
|
79
|
+
@tags.unshift(tag)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def tags_of_type(tag_type)
|
|
83
|
+
tags.select { |t| t.tag_type == tag_type }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def reconcile_tag_ids
|
|
87
|
+
# Ensure tags are loaded so we don't accidentally
|
|
88
|
+
# remove all tagging in situations where tags haven't
|
|
89
|
+
# been accessed during the lifetime of the object
|
|
90
|
+
tags
|
|
91
|
+
|
|
92
|
+
self.tag_ids = @tags.collect(&:tag_id)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def tags
|
|
96
|
+
@tags ||= Tag.by_tag_ids(tag_ids).compact.to_a
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def reload
|
|
100
|
+
@tags = nil
|
|
101
|
+
super
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def save(options={})
|
|
105
|
+
reconcile_tag_ids
|
|
106
|
+
super(options)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def save!(options={})
|
|
110
|
+
reconcile_tag_ids
|
|
111
|
+
super(options)
|
|
112
|
+
end
|
|
113
|
+
end
|