govuk_content_models 6.0.2

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