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.
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
@@ -0,0 +1,40 @@
1
+ require "edition"
2
+
3
+ class SimpleSmartAnswerEdition < Edition
4
+ class Node
5
+ include Mongoid::Document
6
+ embedded_in :edition, :class_name => "SimpleSmartAnswerEdition"
7
+ embeds_many :options, :class_name => "SimpleSmartAnswerEdition::Node::Option"
8
+
9
+ accepts_nested_attributes_for :options, :allow_destroy => true
10
+
11
+ field :slug, type: String
12
+ field :title, type: String
13
+ field :body, type: String
14
+ field :order, type: Integer
15
+ field :kind, type: String
16
+
17
+ default_scope order_by([:order, :asc])
18
+
19
+ GOVSPEAK_FIELDS = [:body]
20
+
21
+ KINDS = [
22
+ 'question',
23
+ 'outcome'
24
+ ]
25
+
26
+ validates :title, :kind, presence: true
27
+ validates :kind, inclusion: { :in => KINDS }
28
+ validates :slug, :presence => true, :format => {:with => /\A[a-z0-9-]+\z/}
29
+
30
+ validate :outcomes_have_no_options
31
+
32
+ validates_with SafeHtml
33
+
34
+ private
35
+
36
+ def outcomes_have_no_options
37
+ errors.add(:options, "cannot be added for an outcome") if options.present? and options.any? and kind == "outcome"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,31 @@
1
+ require "edition"
2
+
3
+ class SimpleSmartAnswerEdition < Edition
4
+ class Node
5
+ class Option
6
+ include Mongoid::Document
7
+
8
+ embedded_in :node, :class_name => "SimpleSmartAnswerEdition::Node"
9
+
10
+ field :label, type: String
11
+ field :slug, type: String
12
+ field :next_node, type: String
13
+ field :order, type: Integer
14
+
15
+ default_scope order_by([:order, :asc])
16
+
17
+ validates :label, :next_node, presence: true
18
+ validates :slug, :format => {:with => /\A[a-z0-9-]+\z/}
19
+
20
+ before_validation :populate_slug_if_blank
21
+
22
+ private
23
+
24
+ def populate_slug_if_blank
25
+ if self.slug.blank? and self.label.present?
26
+ self.slug = ActiveSupport::Inflector.parameterize(self.label)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
data/app/models/tag.rb ADDED
@@ -0,0 +1,88 @@
1
+ require "safe_html"
2
+
3
+ class Tag
4
+ include Mongoid::Document
5
+ field :tag_id, type: String
6
+ field :title, type: String
7
+ field :tag_type, type: String #TODO: list of accepted types?
8
+ field :description, type: String
9
+ field :short_description, type: String
10
+
11
+ field :parent_id, type: String
12
+
13
+ GOVSPEAK_FIELDS = []
14
+
15
+ index :tag_id
16
+ index [ [:tag_id, Mongo::ASCENDING], [:tag_type, Mongo::ASCENDING] ], unique: true
17
+ index :tag_type
18
+
19
+ validates_presence_of :tag_id, :title, :tag_type
20
+ validates_with SafeHtml
21
+
22
+ class MissingTags < RuntimeError
23
+ attr_reader :tag_ids
24
+
25
+ def initialize(tag_ids)
26
+ super("Missing tags: #{tag_ids.join(", ")}")
27
+ @tag_ids = tag_ids
28
+ end
29
+ end
30
+
31
+ # This doesn't get set automatically: the code that loads tags
32
+ # should go through them and set this attribute manually
33
+ attr_accessor :uniquely_named
34
+
35
+ def as_json(options = {})
36
+ {
37
+ id: self.tag_id,
38
+ title: self.title,
39
+ type: self.tag_type,
40
+ description: self.description,
41
+ short_description: self.short_description
42
+ }
43
+ end
44
+
45
+ def has_parent?
46
+ parent_id.present?
47
+ end
48
+
49
+ def parent
50
+ Tag.by_tag_id(parent_id, self.tag_type) if has_parent?
51
+ end
52
+
53
+ def unique_title
54
+ self.uniquely_named ? self.title : "#{self.title} [#{self.tag_id}]"
55
+ end
56
+
57
+ def to_s
58
+ title
59
+ end
60
+
61
+ def self.by_tag_id(tag_id, tag_type = nil)
62
+ scope = tag_type ? Tag.where(tag_type: tag_type) : Tag
63
+ scope.where(tag_id: tag_id).first
64
+ end
65
+
66
+ # Retrieve a list of tags by tag ID. Any missing tags become `nil`.
67
+ def self.by_tag_ids(tag_id_list, tag_type = nil)
68
+ scope = tag_type ? Tag.where(tag_type: tag_type) : Tag
69
+
70
+ # Load up all the tags in a single query
71
+ tags = scope.any_in(tag_id: tag_id_list).to_a
72
+ tag_id_list.map { |tag_id| tags.find { |t| t.tag_id == tag_id } }
73
+ end
74
+
75
+ # Retrieve a list of tags by tag ID. Any missing tags raise an exception.
76
+ def self.by_tag_ids!(tag_id_list, tag_type = nil)
77
+ tags = by_tag_ids(tag_id_list, tag_type)
78
+ if tags.any?(&:nil?)
79
+ # Find the tag IDs for which the resulting tag is nil
80
+ missing_ids = tag_id_list.zip(tags).select { |tag_id, tag|
81
+ tag.nil?
82
+ }.map(&:first)
83
+ raise MissingTags, missing_ids
84
+ else
85
+ tags
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,28 @@
1
+ require "edition"
2
+ require "expectant"
3
+
4
+ class TransactionEdition < Edition
5
+
6
+ include Expectant
7
+
8
+ field :introduction, type: String
9
+ field :will_continue_on, type: String
10
+ field :link, type: String
11
+ field :more_information, type: String
12
+ field :alternate_methods, type: String
13
+
14
+ GOVSPEAK_FIELDS = Edition::GOVSPEAK_FIELDS + [:introduction, :more_information, :alternate_methods]
15
+
16
+ @fields_to_clone = [:introduction, :will_continue_on, :link,
17
+ :more_information, :alternate_methods,
18
+ :minutes_to_complete, :uses_government_gateway,
19
+ :expectation_ids]
20
+
21
+ def indexable_content
22
+ "#{super} #{Govspeak::Document.new(introduction).to_text} #{Govspeak::Document.new(more_information).to_text}".strip
23
+ end
24
+
25
+ def whole_body
26
+ [ self.link, self.introduction, self.more_information ].join("\n\n")
27
+ end
28
+ end
@@ -0,0 +1,177 @@
1
+ require 'attachable'
2
+ require 'parted'
3
+ require 'state_machine'
4
+ require 'safe_html'
5
+ require 'govspeak_smart_quotes_fixer'
6
+
7
+ class TravelAdviceEdition
8
+ include Mongoid::Document
9
+ include Mongoid::Timestamps
10
+ include Parted
11
+ include Attachable
12
+
13
+ field :country_slug, type: String
14
+ field :title, type: String
15
+ field :overview, type: String
16
+ field :version_number, type: Integer
17
+ field :state, type: String, default: "draft"
18
+ field :alert_status, type: Array, default: [ ]
19
+ field :summary, type: String
20
+ field :change_description, type: String
21
+ field :minor_update, type: Boolean, default: false
22
+ field :synonyms, type: Array, default: [ ]
23
+ # This is the publicly presented publish time. For minor updates, this will be the publish time of the previous version
24
+ field :published_at, type: Time
25
+ field :reviewed_at, type: Time
26
+
27
+ embeds_many :actions
28
+
29
+ index [[:country_slug, Mongo::ASCENDING], [:version_number, Mongo::DESCENDING]], :unique => true
30
+
31
+ attaches :image, :document
32
+
33
+ GOVSPEAK_FIELDS = [:summary]
34
+ ALERT_STATUSES = [
35
+ "avoid_all_but_essential_travel_to_parts",
36
+ "avoid_all_but_essential_travel_to_whole_country",
37
+ "avoid_all_travel_to_parts",
38
+ "avoid_all_travel_to_whole_country",
39
+ ]
40
+
41
+ include GovspeakSmartQuotesFixer
42
+ before_validation :populate_version_number, :on => :create
43
+
44
+ validates_presence_of :country_slug, :title
45
+ validate :state_for_slug_unique
46
+ validates :version_number, :presence => true, :uniqueness => { :scope => :country_slug }
47
+ validate :alert_status_contains_valid_values
48
+ validate :first_version_cant_be_minor_update
49
+ validates_with SafeHtml
50
+
51
+ scope :published, where(:state => "published")
52
+
53
+ class << self; attr_accessor :fields_to_clone end
54
+ @fields_to_clone = [:title, :country_slug, :overview, :alert_status, :summary, :image_id, :document_id, :synonyms]
55
+
56
+ state_machine initial: :draft do
57
+ before_transition :draft => :published do |edition, transition|
58
+ if edition.minor_update
59
+ previous = edition.previous_version
60
+ edition.published_at = previous.published_at
61
+ edition.reviewed_at = previous.reviewed_at
62
+ edition.change_description = previous.change_description
63
+ else
64
+ edition.published_at = Time.zone.now.utc
65
+ edition.reviewed_at = edition.published_at
66
+ end
67
+ edition.class.where(country_slug: edition.country_slug, state: 'published').each do |ed|
68
+ ed.archive
69
+ end
70
+ end
71
+
72
+ event :publish do
73
+ transition draft: :published
74
+ end
75
+
76
+ event :archive do
77
+ transition all => :archived, :unless => :archived?
78
+ end
79
+
80
+ state :published do
81
+ validate :cannot_edit_published
82
+ validates_presence_of :change_description, :unless => :minor_update, :message => "can't be blank on publish"
83
+ end
84
+ state :archived do
85
+ validate :cannot_edit_archived
86
+ end
87
+ end
88
+
89
+ def indexable_content
90
+ strings = [Govspeak::Document.new(self.summary).to_text]
91
+ parts.each do |part|
92
+ strings << part.title << Govspeak::Document.new(part.body).to_text
93
+ end
94
+ strings.join(" ").strip
95
+ end
96
+
97
+
98
+ def build_clone
99
+ new_edition = self.class.new
100
+ self.class.fields_to_clone.each do |attr|
101
+ new_edition[attr] = self.read_attribute(attr)
102
+ end
103
+ new_edition.parts = self.parts.map(&:dup)
104
+ new_edition
105
+ end
106
+
107
+ def build_action_as(user, action_type, comment = nil)
108
+ actions.build(:requester => user, :request_type => action_type, :comment => comment)
109
+ end
110
+
111
+ def publish_as(user)
112
+ comment = self.minor_update ? 'Minor update' : Govspeak::Document.new(self.change_description).to_text
113
+ build_action_as(user, Action::PUBLISH, comment) && publish
114
+ end
115
+
116
+ def previous_version
117
+ self.class.where(:country_slug => self.country_slug, :version_number.lt => self.version_number).order_by([:version_number, :desc]).first
118
+ end
119
+
120
+ private
121
+
122
+ def state_for_slug_unique
123
+ if %w(published draft).include?(self.state) and
124
+ self.class.where(:_id.ne => id,
125
+ :country_slug => country_slug,
126
+ :state => state).any?
127
+ errors.add(:state, :taken)
128
+ end
129
+ end
130
+
131
+ def populate_version_number
132
+ if self.version_number.nil? and ! self.country_slug.nil? and ! self.country_slug.empty?
133
+ if latest_edition = self.class.where(:country_slug => self.country_slug).order_by([:version_number, :desc]).first
134
+ self.version_number = latest_edition.version_number + 1
135
+ else
136
+ self.version_number = 1
137
+ end
138
+ end
139
+ end
140
+
141
+ def cannot_edit_published
142
+ if anything_other_than_state_changed?('reviewed_at') and self.state_was != 'draft'
143
+ errors.add(:state, "must be draft to modify")
144
+ end
145
+ end
146
+
147
+ def cannot_edit_archived
148
+ if anything_other_than_state_changed?
149
+ errors.add(:state, "must be draft to modify")
150
+ end
151
+ end
152
+
153
+ def anything_other_than_state_changed?(*additional_allowed_fields)
154
+ self.changed? and ((real_fields_changed - ['state'] - additional_allowed_fields) != [] or self.parts.any?(&:changed?))
155
+ end
156
+
157
+ def real_fields_changed
158
+ # There's an issue with dirty-tracking of Array fields. Merely accessing them will mark
159
+ # them as changed, but with no changes. This recifies that.
160
+ # this also allows changes when the change is something changing from nil to an empty array
161
+ self.changes.reject { |k, v|
162
+ v.nil? || v == [nil, []]
163
+ }.keys
164
+ end
165
+
166
+ def alert_status_contains_valid_values
167
+ self.alert_status.each do |status|
168
+ errors.add(:alert_status, "is not in the list") unless ALERT_STATUSES.include?(status)
169
+ end
170
+ end
171
+
172
+ def first_version_cant_be_minor_update
173
+ if self.minor_update and self.version_number == 1
174
+ errors.add(:minor_update, "can't be set for first version")
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,54 @@
1
+ require "digest/md5"
2
+ require "cgi"
3
+ require "gds-sso/user"
4
+ require "workflow_actor"
5
+ require "safe_html"
6
+
7
+ class User
8
+ include Mongoid::Document
9
+ include Mongoid::Timestamps
10
+ include GDS::SSO::User
11
+ include WorkflowActor
12
+
13
+ # Let an app configure the collection name to use, e.g. set a constant in an
14
+ # initializer
15
+ def self.collection_name
16
+ defined?(USER_COLLECTION_NAME) ? USER_COLLECTION_NAME : "users"
17
+ end
18
+
19
+ field "name", type: String
20
+ field "uid", type: String
21
+ field "version", type: Integer
22
+ field "email", type: String
23
+ field "permissions", type: Array
24
+ field "remotely_signed_out", type: Boolean, default: false
25
+
26
+ GOVSPEAK_FIELDS = []
27
+
28
+ # Setup accessible (or protected) attributes for your model
29
+ attr_accessible :email, :name, :uid
30
+ attr_accessible :email, :name, :uid, :permissions, as: :oauth
31
+
32
+ scope :alphabetized, order_by(name: :asc)
33
+
34
+ validates_with SafeHtml
35
+
36
+ # GDS::SSO specifically looks for find_by_uid within warden
37
+ # when loading authentication user from session
38
+ def self.find_by_uid(uid)
39
+ where(uid: uid).first
40
+ end
41
+
42
+ def to_s
43
+ name || email || ""
44
+ end
45
+
46
+ def gravatar_url(opts = {})
47
+ opts.symbolize_keys!
48
+ "%s.gravatar.com/avatar/%s%s" % [
49
+ opts[:ssl] ? "https://secure" : "http://www",
50
+ Digest::MD5.hexdigest(email.downcase),
51
+ opts[:s] ? "?s=#{CGI.escape(opts[:s])}" : ""
52
+ ]
53
+ end
54
+ end
@@ -0,0 +1,24 @@
1
+ require "edition"
2
+ require "attachable"
3
+
4
+ class VideoEdition < Edition
5
+ include Attachable
6
+
7
+ field :video_url, type: String
8
+ field :video_summary, type: String
9
+ field :body, type: String
10
+
11
+ GOVSPEAK_FIELDS = Edition::GOVSPEAK_FIELDS + [:body]
12
+
13
+ @fields_to_clone = [:video_url, :video_summary, :body]
14
+
15
+ attaches :caption_file
16
+
17
+ def has_video?
18
+ video_url.present?
19
+ end
20
+
21
+ def whole_body
22
+ [video_summary, video_url, body].join("\n\n")
23
+ end
24
+ end