agreements 0.2.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +208 -26
- data/app/models/agreements/version.rb +32 -5
- data/lib/agreements/enforcement.rb +6 -3
- data/lib/agreements/engine.rb +2 -0
- data/lib/agreements/test_helper.rb +20 -0
- data/lib/agreements/version.rb +1 -1
- data/lib/agreements.rb +4 -0
- data/lib/generators/agreements/install/templates/create_agreements_tables.rb.tt +1 -0
- metadata +58 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 921978d679f2c21c42fda41e4caa14e844e5fbfbb57a10c78738811441814547
|
|
4
|
+
data.tar.gz: 342464faa1422920f63ca881f7d64728d35e6b88c7e595d351a06c181a7692cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1cf67625ad815b0e3df2033a609cd510cf9c4ca97888b518c842546bb174b3c22abfd7bfc2670d0756337c3e24465b2e70b4a52887dbbf7e0a1a9c7c78a939b
|
|
7
|
+
data.tar.gz: 60cd61c042671c9bcb86f1e2f3c37ceeba9bfa2600aee3082766fe8b2124a38a7c81bc3b7ae5328f65f6265d6ac8ac6ef2f77416cd3d28547fd1c70bb47607a3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 - 2026-11-12
|
|
4
|
+
|
|
5
|
+
- Add an optional `effective_at` to versions, so a version can be inserted when
|
|
6
|
+
notice is given and become current on the date it takes effect. Requires a
|
|
7
|
+
new nullable column; see "Upgrading from 0.3" in the README.
|
|
8
|
+
- Add `Agreements.upcoming_version` to show the next scheduled version.
|
|
9
|
+
- Add `Agreements::TestHelper#accept_agreement` for host test suites.
|
|
10
|
+
- Explain in the README what acceptance evidence does and does not prove.
|
|
11
|
+
|
|
12
|
+
## 0.3.0 - 2026-11-12
|
|
13
|
+
|
|
14
|
+
- Answer pending non-HTML requests with `403 Forbidden` instead of letting them
|
|
15
|
+
through. `require_agreement` returns `:forbidden` for them.
|
|
16
|
+
- Reject `/\host` return paths, using Rails' `url_from`.
|
|
17
|
+
- Accept an optional `key` on document references for translated link titles.
|
|
18
|
+
- Depend on Action Pack, Active Record, Railties and Global ID instead of all
|
|
19
|
+
of Rails.
|
|
20
|
+
- Document the safe data-migration pattern, Global ID app pinning and a `nil`
|
|
21
|
+
`current_version` on `VersionNotCurrent`.
|
|
22
|
+
|
|
3
23
|
## 0.2.0 - 2026-08-27
|
|
4
24
|
|
|
5
25
|
- Record the exact localized acceptance statement and locale shown by the host.
|
data/README.md
CHANGED
|
@@ -29,6 +29,23 @@ view, initializer, JavaScript, CSS, or admin dashboard to integrate.
|
|
|
29
29
|
|
|
30
30
|
Ruby >= 3.2 · Rails >= 7.1 and < 9 · PostgreSQL and SQLite
|
|
31
31
|
|
|
32
|
+
### Upgrading from 0.3
|
|
33
|
+
|
|
34
|
+
Version 0.4 adds a nullable `effective_at` column to `agreements_versions`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bin/rails generate migration AddEffectiveAtToAgreementsVersions effective_at:datetime
|
|
38
|
+
bin/rails db:migrate
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Existing rows keep `effective_at` empty and stay in effect from their creation.
|
|
42
|
+
|
|
43
|
+
### Upgrading from 0.2
|
|
44
|
+
|
|
45
|
+
Version 0.3 has no schema change. `require_agreement` now answers a pending
|
|
46
|
+
non-HTML request with `403 Forbidden` instead of letting it through. If your
|
|
47
|
+
API already checks `pending_version` before calling it, nothing changes.
|
|
48
|
+
|
|
32
49
|
### Upgrading from 0.1
|
|
33
50
|
|
|
34
51
|
Version 0.2 adds non-null `acceptance_statement` and `locale` columns to
|
|
@@ -38,29 +55,92 @@ locale such as `und` when the displayed locale is unknown.
|
|
|
38
55
|
|
|
39
56
|
## Define a version
|
|
40
57
|
|
|
41
|
-
Agreement versions are deployed data.
|
|
42
|
-
the same
|
|
58
|
+
Agreement versions are deployed data. Insert them in a data migration, and keep
|
|
59
|
+
the same list in your seeds so fresh databases match production:
|
|
43
60
|
|
|
44
61
|
```ruby
|
|
45
|
-
|
|
62
|
+
class AddUserTerms20260816 < ActiveRecord::Migration[8.1]
|
|
63
|
+
class Version < ActiveRecord::Base
|
|
64
|
+
self.table_name = "agreements_versions"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
VERSION = {
|
|
68
|
+
agreement_key: "user_terms",
|
|
69
|
+
version: "2026-08-16",
|
|
70
|
+
acceptance_statement: "I accept the Terms of Service and acknowledge the Privacy Notice.",
|
|
71
|
+
documents: [
|
|
72
|
+
{ key: "terms_of_service", title: "Terms of Service", url: "https://example.com/terms-2026-08-16" },
|
|
73
|
+
{ key: "privacy_notice", title: "Privacy Notice", url: "https://example.com/privacy-2026-08-16" }
|
|
74
|
+
]
|
|
75
|
+
}.freeze
|
|
76
|
+
|
|
77
|
+
def up
|
|
78
|
+
version = Version.find_or_create_by!(VERSION.slice(:agreement_key, :version)) do |record|
|
|
79
|
+
record.assign_attributes(VERSION)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
unless version.acceptance_statement == VERSION[:acceptance_statement] &&
|
|
83
|
+
version.documents == VERSION[:documents].as_json
|
|
84
|
+
raise "#{version.agreement_key} #{version.version} already exists with different content"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def down
|
|
89
|
+
raise ActiveRecord::IrreversibleMigration, "Agreement versions are evidence"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The migration defines its own model so it keeps working when the gem's model
|
|
95
|
+
changes later. The final check stops a deploy if a version label already exists
|
|
96
|
+
with different content: a deployed version must never change. Use the same
|
|
97
|
+
create-then-check loop in your seeds.
|
|
98
|
+
|
|
99
|
+
A version without `effective_at` is current as soon as it is inserted. There is
|
|
100
|
+
no draft, publish or activate step. A correction or legal update is a new
|
|
101
|
+
immutable row with a new version label.
|
|
102
|
+
|
|
103
|
+
### Give notice before a version takes effect
|
|
104
|
+
|
|
105
|
+
Terms often promise notice before a change applies, for example "at least 14
|
|
106
|
+
days before the changes take effect". Insert the version when you send the
|
|
107
|
+
notice, with the date it takes effect:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
VERSION = {
|
|
46
111
|
agreement_key: "user_terms",
|
|
47
|
-
version: "2026-
|
|
48
|
-
|
|
49
|
-
documents
|
|
50
|
-
|
|
51
|
-
{ title: "Privacy Notice", url: "https://example.com/privacy" }
|
|
52
|
-
]
|
|
53
|
-
)
|
|
112
|
+
version: "2026-12-01",
|
|
113
|
+
effective_at: Time.utc(2026, 12, 1),
|
|
114
|
+
# acceptance_statement, documents ...
|
|
115
|
+
}.freeze
|
|
54
116
|
```
|
|
55
117
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
118
|
+
Until that time the previous version stays current, nobody is asked to accept,
|
|
119
|
+
and `accept!` refuses the new version. From that time the new version is
|
|
120
|
+
current and every subject who has not accepted it is pending. Show the notice
|
|
121
|
+
in your app with:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
Agreements.upcoming_version("user_terms") # => the next scheduled version, or nil
|
|
125
|
+
```
|
|
59
126
|
|
|
60
|
-
|
|
61
|
-
`
|
|
62
|
-
|
|
63
|
-
|
|
127
|
+
The current version is the one that took effect most recently: its
|
|
128
|
+
`effective_at`, or its creation time when `effective_at` is empty. When two
|
|
129
|
+
versions take effect at the same time, the one inserted later wins.
|
|
130
|
+
|
|
131
|
+
Subjects cannot accept a scheduled version early. Someone who accepts the
|
|
132
|
+
current version during the notice period is asked again on the effective date.
|
|
133
|
+
|
|
134
|
+
A scheduled version cannot have acceptances before it takes effect. To cancel
|
|
135
|
+
it, delete it in a data migration with a migration-local model before its
|
|
136
|
+
effective date. After that date it is evidence like any other version.
|
|
137
|
+
|
|
138
|
+
Document references accept `title`, an HTTPS `url`, an optional 64-character
|
|
139
|
+
`sha256`, and an optional `key`. Use `key` (lowercase letters, digits and
|
|
140
|
+
underscores) to look up a translated link title, so your views do not depend on
|
|
141
|
+
the English `title`. The gem validates a supplied digest but does not fetch
|
|
142
|
+
remote pages, calculate hashes, or retain document bytes. Hashing and archival
|
|
143
|
+
belong in the trusted legal-document publishing process.
|
|
64
144
|
|
|
65
145
|
## Ask what is pending
|
|
66
146
|
|
|
@@ -73,6 +153,19 @@ Subjects and actors may be records responding to `to_global_id`, or explicit
|
|
|
73
153
|
non-empty opaque strings. The gem stores only the resulting keys; it does not
|
|
74
154
|
own authentication, tenancy, roles, or authorization.
|
|
75
155
|
|
|
156
|
+
A Global ID contains your application name and the model class, for example
|
|
157
|
+
`gid://my-app/User/1`. If either changes, old acceptances no longer match and
|
|
158
|
+
every subject is asked to accept again. Pin the application name now so a
|
|
159
|
+
module rename cannot change it:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
# config/application.rb
|
|
163
|
+
config.global_id.app = "my-app"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Before you rename a model, plan how to carry its acceptances over. Acceptance
|
|
167
|
+
rows are read only through the model API.
|
|
168
|
+
|
|
76
169
|
## Render the host-owned form
|
|
77
170
|
|
|
78
171
|
Keep the page, routes, authorization, document-link markup, and copy in your
|
|
@@ -106,10 +199,13 @@ host. Do not accept either value from browser parameters. Retries, double
|
|
|
106
199
|
clicks, and concurrent submissions return the original acceptance evidence.
|
|
107
200
|
|
|
108
201
|
A missing, malformed, wrong-agreement, or stale ID raises
|
|
109
|
-
`Agreements::VersionNotCurrent`. Its `current_version` is ready to render
|
|
202
|
+
`Agreements::VersionNotCurrent`. Its `current_version` is ready to render. It
|
|
203
|
+
is `nil` when no version exists for that key yet:
|
|
110
204
|
|
|
111
205
|
```ruby
|
|
112
206
|
rescue Agreements::VersionNotCurrent => error
|
|
207
|
+
raise ActiveRecord::RecordNotFound unless error.current_version
|
|
208
|
+
|
|
113
209
|
@version = error.current_version
|
|
114
210
|
render :show, status: :unprocessable_content
|
|
115
211
|
end
|
|
@@ -156,16 +252,68 @@ def require_organization_dpa
|
|
|
156
252
|
end
|
|
157
253
|
```
|
|
158
254
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
255
|
+
When the agreement is pending:
|
|
256
|
+
|
|
257
|
+
- an HTML or Turbo Stream request is redirected to `location` with
|
|
258
|
+
`303 See Other`, and the method returns `:redirected`;
|
|
259
|
+
- any other request (JSON, CSV, PDF, API) gets an empty `403 Forbidden`, and
|
|
260
|
+
the method returns `:forbidden`.
|
|
261
|
+
|
|
262
|
+
The return value lets you skip work that should not run after a halted
|
|
263
|
+
request, such as Pundit's `verify_authorized`:
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
skip_authorization if require_agreement("organization_dpa", subject: Current.organization, location: path)
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Only GET and HEAD return locations are remembered. A blocked mutation is never
|
|
270
|
+
replayed after acceptance. Consume the safe same-origin path after a successful
|
|
271
|
+
acceptance:
|
|
162
272
|
|
|
163
273
|
```ruby
|
|
164
274
|
redirect_to agreement_return_location || dashboard_path
|
|
165
275
|
```
|
|
166
276
|
|
|
167
|
-
API
|
|
168
|
-
|
|
277
|
+
If your API has its own error format, check before `require_agreement` runs:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
def require_organization_dpa
|
|
281
|
+
if api_request?
|
|
282
|
+
if Agreements.pending_version("organization_dpa", subject: Current.organization)
|
|
283
|
+
render json: { error: "agreement_required" }, status: :forbidden
|
|
284
|
+
end
|
|
285
|
+
return
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
require_agreement("organization_dpa", subject: Current.organization, location: organization_dpa_path(Current.organization))
|
|
289
|
+
end
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
When no version exists for a key, nothing is pending and nothing is blocked.
|
|
293
|
+
Insert the first version before you rely on enforcement.
|
|
294
|
+
|
|
295
|
+
## Test your application
|
|
296
|
+
|
|
297
|
+
Once enforcement is on, most of your tests need subjects that have already
|
|
298
|
+
accepted. Include the test helper:
|
|
299
|
+
|
|
300
|
+
```ruby
|
|
301
|
+
# test/test_helper.rb
|
|
302
|
+
require "agreements/test_helper"
|
|
303
|
+
|
|
304
|
+
class ActiveSupport::TestCase
|
|
305
|
+
include Agreements::TestHelper
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
```ruby
|
|
310
|
+
accept_agreement("user_terms", subject: user)
|
|
311
|
+
accept_agreement("organization_dpa", subject: organization, actor: owner, authority: "organization_owner")
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
`accept_agreement` accepts the current version with its canonical statement and
|
|
315
|
+
the `en` locale. It raises when no current version exists, so insert versions
|
|
316
|
+
in your fixtures or setup first.
|
|
169
317
|
|
|
170
318
|
## Evidence model
|
|
171
319
|
|
|
@@ -175,6 +323,7 @@ the response contract your application supports.
|
|
|
175
323
|
- human version label;
|
|
176
324
|
- exact acceptance statement;
|
|
177
325
|
- external document references and optional SHA-256 digests;
|
|
326
|
+
- optional effective date;
|
|
178
327
|
- timestamps.
|
|
179
328
|
|
|
180
329
|
`agreements_acceptances` contains:
|
|
@@ -204,12 +353,43 @@ controls.
|
|
|
204
353
|
|
|
205
354
|
1. Publish immutable/versioned legal-document URLs and any verified digests.
|
|
206
355
|
2. Add the finished bundle to the host's seed registry.
|
|
207
|
-
3. Add a host data migration that inserts the same immutable version
|
|
208
|
-
|
|
356
|
+
3. Add a host data migration that inserts the same immutable version, with
|
|
357
|
+
`effective_at` if your terms promise notice.
|
|
358
|
+
4. Deploy. The new row becomes current at once, or on its effective date, and
|
|
359
|
+
subjects missing it are prompted.
|
|
209
360
|
|
|
210
361
|
Production deployments normally run migrations, not seeds. Changing a seed
|
|
211
362
|
registry alone does not deploy a new version to an existing installation.
|
|
212
363
|
|
|
364
|
+
## What the evidence proves
|
|
365
|
+
|
|
366
|
+
The gem records which version and which sentence a subject accepted, who did
|
|
367
|
+
it, under what authority, and when. That is the part your application can
|
|
368
|
+
prove later. Some questions it cannot answer for you:
|
|
369
|
+
|
|
370
|
+
- **The whole page counts.** Courts look at the entire screen, not only the
|
|
371
|
+
checkbox. In *Berman v. Freedom Financial Network* (9th Cir. 2022) the terms
|
|
372
|
+
did not bind the user: the notice was small grey text, the links did not
|
|
373
|
+
look like links, and the button said "Continue" without saying that it meant
|
|
374
|
+
acceptance. Your layout, contrast and button text decide this. The gem does
|
|
375
|
+
not see them.
|
|
376
|
+
- **It is not a qualified electronic signature.** Under eIDAS (Regulation (EU)
|
|
377
|
+
No 910/2014, Article 25), an electronic signature cannot be denied legal
|
|
378
|
+
effect only because it is electronic or not qualified. A checkbox is still
|
|
379
|
+
not a qualified electronic signature, and this gem does not create one.
|
|
380
|
+
- **Acknowledging a privacy notice is not consent.** Under the GDPR you inform
|
|
381
|
+
people (Article 13). Consent (Articles 6 and 7) is a separate, specific and
|
|
382
|
+
withdrawable choice. Keep the two apart in your copy and your records.
|
|
383
|
+
- **The text of your terms still matters.** In the EU, an unfair term in a
|
|
384
|
+
consumer contract does not bind the consumer (Directive 93/13/EEC), however
|
|
385
|
+
clearly it was accepted.
|
|
386
|
+
- **Identity and authority are yours to check.** The gem stores the actor and
|
|
387
|
+
the authority you pass. It does not verify who the person is or whether they
|
|
388
|
+
may bind an organization.
|
|
389
|
+
|
|
390
|
+
Your counsel decides what your agreements need. The gem keeps the record of
|
|
391
|
+
what was accepted.
|
|
392
|
+
|
|
213
393
|
## Not this gem
|
|
214
394
|
|
|
215
395
|
- Legal-document hosting, editing, rendering, uploads, or a CMS.
|
|
@@ -223,7 +403,9 @@ registry alone does not deploy a new version to an existing installation.
|
|
|
223
403
|
layouts, routes, or copy.
|
|
224
404
|
|
|
225
405
|
Those are real product categories. They are not prerequisites for proving an
|
|
226
|
-
ordinary Terms or organization-DPA acceptance.
|
|
406
|
+
ordinary Terms or organization-DPA acceptance. If you need consent withdrawal,
|
|
407
|
+
signed receipts, request evidence or a form builder, look at
|
|
408
|
+
[clickwrap](https://github.com/rameerez/clickwrap).
|
|
227
409
|
|
|
228
410
|
## Later, only after a concrete trigger
|
|
229
411
|
|
|
@@ -5,6 +5,7 @@ require "uri"
|
|
|
5
5
|
module Agreements
|
|
6
6
|
class Version < ApplicationRecord
|
|
7
7
|
SHA256_PATTERN = /\A[0-9a-f]{64}\z/i
|
|
8
|
+
KEY_PATTERN = /\A[a-z0-9_]+\z/
|
|
8
9
|
|
|
9
10
|
has_many :acceptances,
|
|
10
11
|
class_name: "Agreements::Acceptance",
|
|
@@ -12,12 +13,33 @@ module Agreements
|
|
|
12
13
|
inverse_of: :agreement_version,
|
|
13
14
|
dependent: :restrict_with_exception
|
|
14
15
|
|
|
15
|
-
validates :agreement_key, presence: true, format: { with:
|
|
16
|
+
validates :agreement_key, presence: true, format: { with: KEY_PATTERN }, uniqueness: { scope: :version }
|
|
16
17
|
validates :version, :acceptance_statement, presence: true
|
|
17
18
|
validate :documents_are_external_references
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
# Filtering by time in Ruby keeps the SQL identical, so the query cache
|
|
21
|
+
# serves repeated checks within a request. Each key has only a few versions.
|
|
22
|
+
def self.current_for(agreement_key, now: Time.current)
|
|
23
|
+
versions = where(agreement_key: agreement_key).to_a
|
|
24
|
+
versions.select { |version| version.in_effect?(now) }.max_by(&:precedence)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.upcoming_for(agreement_key, now: Time.current)
|
|
28
|
+
scheduled = where(agreement_key: agreement_key).to_a.reject { |version| version.in_effect?(now) }
|
|
29
|
+
next_from = scheduled.map(&:effective_from).min
|
|
30
|
+
scheduled.select { |version| version.effective_from == next_from }.max_by(&:precedence)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def precedence
|
|
34
|
+
[effective_from, created_at, id]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def effective_from
|
|
38
|
+
effective_at || created_at
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def in_effect?(now = Time.current)
|
|
42
|
+
effective_from <= now
|
|
21
43
|
end
|
|
22
44
|
|
|
23
45
|
def accepted_by?(subject)
|
|
@@ -51,13 +73,18 @@ module Agreements
|
|
|
51
73
|
end
|
|
52
74
|
|
|
53
75
|
def allowed_document_attributes?(attributes)
|
|
54
|
-
attributes.keys.all? { |key| %w[title url sha256].include?(key) }
|
|
76
|
+
attributes.keys.all? { |key| %w[key title url sha256].include?(key) }
|
|
55
77
|
end
|
|
56
78
|
|
|
57
79
|
def complete_document_attributes?(attributes)
|
|
58
80
|
attributes["title"].present? &&
|
|
59
81
|
external_https_url?(attributes["url"]) &&
|
|
60
|
-
valid_sha256?(attributes["sha256"])
|
|
82
|
+
valid_sha256?(attributes["sha256"]) &&
|
|
83
|
+
valid_document_key?(attributes["key"])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def valid_document_key?(key)
|
|
87
|
+
key.nil? || key.to_s.match?(KEY_PATTERN)
|
|
61
88
|
end
|
|
62
89
|
|
|
63
90
|
def external_https_url?(url)
|
|
@@ -9,9 +9,13 @@ module Agreements
|
|
|
9
9
|
private
|
|
10
10
|
|
|
11
11
|
def require_agreement(agreement_key, subject:, location:)
|
|
12
|
-
return unless agreement_enforcement_request?
|
|
13
12
|
return unless Agreements.pending_version(agreement_key, subject: subject)
|
|
14
13
|
|
|
14
|
+
unless agreement_enforcement_request?
|
|
15
|
+
head :forbidden
|
|
16
|
+
return :forbidden
|
|
17
|
+
end
|
|
18
|
+
|
|
15
19
|
remember_agreement_return_location
|
|
16
20
|
redirect_to location, status: :see_other
|
|
17
21
|
:redirected
|
|
@@ -30,8 +34,7 @@ module Agreements
|
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def agreement_return_location
|
|
33
|
-
|
|
34
|
-
path if path&.start_with?("/") && !path.start_with?("//")
|
|
37
|
+
url_from(session.delete(:return_to_after_agreement))
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
end
|
data/lib/agreements/engine.rb
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Agreements
|
|
4
|
+
module TestHelper
|
|
5
|
+
def accept_agreement(agreement_key, subject:, actor: subject, authority: "self", locale: "en")
|
|
6
|
+
version = Agreements.current_version(agreement_key)
|
|
7
|
+
raise ArgumentError, "no current #{agreement_key} version; insert one in your test setup" unless version
|
|
8
|
+
|
|
9
|
+
Agreements.accept!(
|
|
10
|
+
agreement_key,
|
|
11
|
+
version_id: version.id,
|
|
12
|
+
subject: subject,
|
|
13
|
+
actor: actor,
|
|
14
|
+
authority: authority,
|
|
15
|
+
acceptance_statement: version.acceptance_statement,
|
|
16
|
+
locale: locale
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
data/lib/agreements/version.rb
CHANGED
data/lib/agreements.rb
CHANGED
|
@@ -17,6 +17,10 @@ module Agreements
|
|
|
17
17
|
Version.current_for(agreement_key)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def upcoming_version(agreement_key)
|
|
21
|
+
Version.upcoming_for(agreement_key)
|
|
22
|
+
end
|
|
23
|
+
|
|
20
24
|
def pending_version(agreement_key, subject:)
|
|
21
25
|
current_version(agreement_key).then do |version|
|
|
22
26
|
version unless version.nil? || version.accepted_by?(subject)
|
|
@@ -7,6 +7,7 @@ class CreateAgreementsTables < ActiveRecord::Migration<%= migration_version %>
|
|
|
7
7
|
t.string :version, null: false
|
|
8
8
|
t.text :acceptance_statement, null: false
|
|
9
9
|
t.json :documents, null: false, default: []
|
|
10
|
+
t.datetime :effective_at
|
|
10
11
|
t.timestamps
|
|
11
12
|
|
|
12
13
|
t.index %i[agreement_key version], unique: true
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: agreements
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -10,7 +10,61 @@ cert_chain: []
|
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
|
-
name:
|
|
13
|
+
name: actionpack
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '7.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: activerecord
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.1'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '9'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '7.1'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '9'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: globalid
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '1.0'
|
|
59
|
+
type: :runtime
|
|
60
|
+
prerelease: false
|
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '1.0'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: railties
|
|
14
68
|
requirement: !ruby/object:Gem::Requirement
|
|
15
69
|
requirements:
|
|
16
70
|
- - ">="
|
|
@@ -54,6 +108,7 @@ files:
|
|
|
54
108
|
- lib/agreements/errors.rb
|
|
55
109
|
- lib/agreements/identity.rb
|
|
56
110
|
- lib/agreements/recorder.rb
|
|
111
|
+
- lib/agreements/test_helper.rb
|
|
57
112
|
- lib/agreements/version.rb
|
|
58
113
|
- lib/generators/agreements/install/install_generator.rb
|
|
59
114
|
- lib/generators/agreements/install/templates/create_agreements_tables.rb.tt
|
|
@@ -81,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
81
136
|
- !ruby/object:Gem::Version
|
|
82
137
|
version: '0'
|
|
83
138
|
requirements: []
|
|
84
|
-
rubygems_version: 4.0.
|
|
139
|
+
rubygems_version: 4.0.20
|
|
85
140
|
specification_version: 4
|
|
86
141
|
summary: Auditable acceptance of externally hosted legal agreements for Rails.
|
|
87
142
|
test_files: []
|