agreement_engine 0.1.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 +7 -0
- data/.github/workflows/ci.yml +22 -0
- data/.github/workflows/release.yml +15 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +19 -0
- data/LICENSE +21 -0
- data/README.md +104 -0
- data/Rakefile +8 -0
- data/app/controllers/agreement_engine/admin/documents_controller.rb +71 -0
- data/app/controllers/agreement_engine/agreements_controller.rb +57 -0
- data/app/controllers/agreement_engine/documents_controller.rb +15 -0
- data/app/models/agreement_engine/document.rb +27 -0
- data/app/views/agreement_engine/admin/documents/_form.html.erb +44 -0
- data/app/views/agreement_engine/admin/documents/edit.html.erb +5 -0
- data/app/views/agreement_engine/admin/documents/index.html.erb +38 -0
- data/app/views/agreement_engine/admin/documents/new.html.erb +5 -0
- data/app/views/agreement_engine/admin/documents/show.html.erb +30 -0
- data/app/views/agreement_engine/agreements/show.html.erb +24 -0
- data/app/views/agreement_engine/documents/privacy.html.erb +16 -0
- data/app/views/agreement_engine/documents/terms.html.erb +16 -0
- data/app/views/layouts/agreement_engine/minimal.html.erb +11 -0
- data/config/locales/en.yml +11 -0
- data/config/routes.rb +10 -0
- data/db/migrate/20260208000001_create_fine_print_documents.rb +15 -0
- data/db/migrate/20260208000002_add_fine_print_to_users.rb +8 -0
- data/db/migrate/20260921200000_rename_fine_print_tables_to_agreement_engine.rb +5 -0
- data/lib/agreement_engine/agreement.rb +23 -0
- data/lib/agreement_engine/configuration.rb +36 -0
- data/lib/agreement_engine/enforceable.rb +22 -0
- data/lib/agreement_engine/engine.rb +13 -0
- data/lib/agreement_engine/signable.rb +23 -0
- data/lib/agreement_engine/version.rb +5 -0
- data/lib/agreement_engine.rb +21 -0
- data/lib/generators/agreement_engine/install_generator.rb +32 -0
- data/lib/generators/agreement_engine/templates/initializer.rb +18 -0
- data/lib/tasks/agreement_engine.rake +23 -0
- metadata +106 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 20b626073ba02a1bbcdb3512d51adae2aaffefd73eb31f1f1dc9a985969d89f0
|
|
4
|
+
data.tar.gz: 33445687584c472677c84c26d3e8e3dc151e911c8b3f375b7a15c7fe6f9854a4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 33bde405dbcf745dcfced2ddf15280b9491ddcc22dde298a14b8a6ebdbe6f7721fa00413f1e01b2b81fd16144590d6462a427c3bf61b962bab03ba8d89d13225
|
|
7
|
+
data.tar.gz: 8afb74c402eda31255db66d590b64a52661ed3b734bddfa42e6da518216f95bf9aaf29916c0f1f55b70ca92afee32bd56581cdb47b6539c2ca25428ca16d66c1
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
branches: [ main ]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout code
|
|
14
|
+
uses: actions/checkout@v6
|
|
15
|
+
|
|
16
|
+
- name: Set up Ruby
|
|
17
|
+
uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
bundler-cache: true
|
|
20
|
+
|
|
21
|
+
- name: Run tests
|
|
22
|
+
run: bundle exec rake test
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main ]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
permissions:
|
|
11
|
+
contents: write
|
|
12
|
+
id-token: write
|
|
13
|
+
issues: write
|
|
14
|
+
actions: read
|
|
15
|
+
uses: DYB-Development/symphony/.github/workflows/gem-release.yml@main
|
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.4.6
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-02-12
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Rails engine with isolated namespace
|
|
10
|
+
- `AgreementEngine::Document` model with versioned legal document management
|
|
11
|
+
- `Signable` concern for user models to track agreement acceptance
|
|
12
|
+
- `Enforceable` concern for controllers to require agreement acceptance
|
|
13
|
+
- `AgreementsController` for user-facing acceptance flow
|
|
14
|
+
- `DocumentsController` with public terms and privacy pages
|
|
15
|
+
- `Admin::DocumentsController` with full CRUD and auth protection
|
|
16
|
+
- Engine migrations for documents table and user tracking columns
|
|
17
|
+
- Seed rake task (`agreement_engine:seed`) for initial legal documents
|
|
18
|
+
- Host app route helpers exposed to engine views
|
|
19
|
+
- GitHub Actions CI workflow
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tyler Schneider
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# AgreementEngine
|
|
2
|
+
|
|
3
|
+
Versioned legal document management for Rails. Track and enforce user acceptance of Terms of Service, Privacy Policy, and other legal agreements with versioned documents and audit trails.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add to your Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "agreement_engine"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
rails agreement_engine:install:migrations
|
|
18
|
+
rails db:migrate
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Setup
|
|
22
|
+
|
|
23
|
+
### 1. Configure agreements
|
|
24
|
+
|
|
25
|
+
Create `config/initializers/agreement_engine.rb`:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
AgreementEngine.configure do |config|
|
|
29
|
+
config.agreements = [
|
|
30
|
+
AgreementEngine::Agreement.new(
|
|
31
|
+
id: :terms_of_service,
|
|
32
|
+
title: "Terms Of Service",
|
|
33
|
+
version_column: :accepted_terms_of_service_version_id,
|
|
34
|
+
document_type: :terms_of_service,
|
|
35
|
+
prompt_when_updated: true
|
|
36
|
+
),
|
|
37
|
+
AgreementEngine::Agreement.new(
|
|
38
|
+
id: :privacy_policy,
|
|
39
|
+
title: "Privacy Policy",
|
|
40
|
+
version_column: :accepted_privacy_policy_version_id,
|
|
41
|
+
document_type: :privacy_policy,
|
|
42
|
+
prompt_when_updated: true
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Include concerns
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
# app/models/user.rb
|
|
52
|
+
class User < ApplicationRecord
|
|
53
|
+
include AgreementEngine::Signable
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# app/controllers/application_controller.rb
|
|
57
|
+
class ApplicationController < ActionController::Base
|
|
58
|
+
include AgreementEngine::Enforceable
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 3. Mount routes
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
# config/routes.rb
|
|
66
|
+
mount AgreementEngine::Engine, at: "/"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Seed initial documents
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
rails agreement_engine:seed
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Auth Configuration
|
|
76
|
+
|
|
77
|
+
AgreementEngine defaults to Devise conventions. Override for other auth systems:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
AgreementEngine.configure do |config|
|
|
81
|
+
config.current_user_method = ->(c) { c.current_user }
|
|
82
|
+
config.signed_in_method = ->(c) { c.user_signed_in? }
|
|
83
|
+
config.auth_controller_method = ->(c) { c.devise_controller? }
|
|
84
|
+
config.sign_out_method = ->(c) { c.sign_out(c.current_user) }
|
|
85
|
+
end
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Routes
|
|
89
|
+
|
|
90
|
+
The engine provides:
|
|
91
|
+
|
|
92
|
+
- `GET /agreements/:id` - Show agreement for acceptance
|
|
93
|
+
- `PATCH /agreements/:id` - Accept agreement
|
|
94
|
+
- `DELETE /agreements/:id` - Decline agreement (signs user out)
|
|
95
|
+
- `GET /terms` - Public terms of service page
|
|
96
|
+
- `GET /privacy` - Public privacy policy page
|
|
97
|
+
- `GET /admin/documents` - Admin CRUD for managing document versions
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
bundle install
|
|
103
|
+
bundle exec rake test
|
|
104
|
+
```
|
data/Rakefile
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
module Admin
|
|
5
|
+
class DocumentsController < ::ApplicationController
|
|
6
|
+
skip_before_action :require_accepted_agreements!, raise: false
|
|
7
|
+
before_action :authenticate_admin!
|
|
8
|
+
before_action :set_document, only: [:show, :edit, :update, :destroy]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
@documents = AgreementEngine::Document.order(created_at: :desc)
|
|
12
|
+
@documents = apply_scope(@documents)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def show
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def new
|
|
19
|
+
@document = AgreementEngine::Document.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def create
|
|
23
|
+
@document = AgreementEngine::Document.new(document_params)
|
|
24
|
+
|
|
25
|
+
if @document.save
|
|
26
|
+
redirect_to agreement_engine.admin_document_path(@document), notice: "Document was successfully created."
|
|
27
|
+
else
|
|
28
|
+
render :new, status: :unprocessable_entity
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def edit
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def update
|
|
36
|
+
if @document.update(document_params)
|
|
37
|
+
redirect_to agreement_engine.admin_document_path(@document), notice: "Document was successfully updated."
|
|
38
|
+
else
|
|
39
|
+
render :edit, status: :unprocessable_entity
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def destroy
|
|
44
|
+
@document.destroy
|
|
45
|
+
redirect_to agreement_engine.admin_documents_path, notice: "Document was successfully deleted.", status: :see_other
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def authenticate_admin!
|
|
51
|
+
AgreementEngine.config.authenticate_admin!(self)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def set_document
|
|
55
|
+
@document = AgreementEngine::Document.find(params[:id])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def document_params
|
|
59
|
+
params.require(:document).permit(:document_type, :version, :summary, :effective_at, :content)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def apply_scope(documents)
|
|
63
|
+
case params[:scope]
|
|
64
|
+
when "published" then documents.published
|
|
65
|
+
when "draft" then documents.draft
|
|
66
|
+
else documents
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
class AgreementsController < ::ApplicationController
|
|
5
|
+
skip_before_action :require_accepted_agreements!, raise: false
|
|
6
|
+
before_action :agreement_engine_authenticate_user!
|
|
7
|
+
before_action :set_agreement
|
|
8
|
+
before_action :set_version
|
|
9
|
+
|
|
10
|
+
layout "agreement_engine/minimal"
|
|
11
|
+
|
|
12
|
+
def show
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def update
|
|
16
|
+
if @version
|
|
17
|
+
agreement_engine_current_user.accept_version!(@agreement, @version)
|
|
18
|
+
end
|
|
19
|
+
redirect_to after_accepted_path, status: :see_other
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def destroy
|
|
23
|
+
AgreementEngine.config.sign_out(self)
|
|
24
|
+
redirect_to main_app.root_path, status: :see_other, alert: t(".declined", agreement: @agreement.title)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def agreement_engine_current_user
|
|
30
|
+
AgreementEngine.config.current_user(self)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def agreement_engine_authenticate_user!
|
|
34
|
+
unless AgreementEngine.config.signed_in?(self)
|
|
35
|
+
redirect_to main_app.root_path
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_agreement
|
|
40
|
+
@agreement = AgreementEngine.config.agreements.find { |a| a.id.to_s == params[:id] }
|
|
41
|
+
|
|
42
|
+
if @agreement.nil?
|
|
43
|
+
redirect_to main_app.root_path
|
|
44
|
+
elsif @agreement.accepted_by?(agreement_engine_current_user)
|
|
45
|
+
redirect_to after_accepted_path
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def set_version
|
|
50
|
+
@version = @agreement&.current_version
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def after_accepted_path
|
|
54
|
+
stored_location_for(:user) || main_app.root_path
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
class DocumentsController < ::ApplicationController
|
|
5
|
+
def terms
|
|
6
|
+
@agreement = AgreementEngine.config.agreements.find { |a| a.id == :terms_of_service }
|
|
7
|
+
@version = AgreementEngine::Document.current(:terms_of_service)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def privacy
|
|
11
|
+
@agreement = AgreementEngine.config.agreements.find { |a| a.id == :privacy_policy }
|
|
12
|
+
@version = AgreementEngine::Document.current(:privacy_policy)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
class Document < ActiveRecord::Base
|
|
5
|
+
self.table_name = "agreement_engine_documents"
|
|
6
|
+
|
|
7
|
+
has_rich_text :content
|
|
8
|
+
|
|
9
|
+
validates :document_type, presence: true
|
|
10
|
+
validates :version, presence: true, uniqueness: {scope: :document_type}
|
|
11
|
+
validates :content, presence: true
|
|
12
|
+
|
|
13
|
+
scope :published, -> { where("effective_at <= ?", Time.current) }
|
|
14
|
+
scope :draft, -> { where(effective_at: nil) }
|
|
15
|
+
|
|
16
|
+
def self.current(document_type)
|
|
17
|
+
where(document_type: document_type)
|
|
18
|
+
.published
|
|
19
|
+
.order(effective_at: :desc)
|
|
20
|
+
.first
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def published?
|
|
24
|
+
effective_at.present? && effective_at <= Time.current
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<%= form_with(model: document, url: url, class: "space-y-4") do |form| %>
|
|
2
|
+
<% if document.errors.any? %>
|
|
3
|
+
<div class="bg-red-50 border border-red-200 rounded p-4">
|
|
4
|
+
<h2 class="text-red-800 font-medium"><%= pluralize(document.errors.count, "error") %> prohibited this document from being saved:</h2>
|
|
5
|
+
<ul class="mt-2 list-disc list-inside text-red-700">
|
|
6
|
+
<% document.errors.full_messages.each do |message| %>
|
|
7
|
+
<li><%= message %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
10
|
+
</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
|
|
13
|
+
<div>
|
|
14
|
+
<%= form.label :document_type, class: "block text-sm font-medium text-gray-700" %>
|
|
15
|
+
<%= form.select :document_type,
|
|
16
|
+
AgreementEngine.config.agreements.map { |a| [a.title, a.document_type] },
|
|
17
|
+
{}, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm" %>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div>
|
|
21
|
+
<%= form.label :version, class: "block text-sm font-medium text-gray-700" %>
|
|
22
|
+
<%= form.text_field :version, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm" %>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div>
|
|
26
|
+
<%= form.label :summary, class: "block text-sm font-medium text-gray-700" %>
|
|
27
|
+
<%= form.text_area :summary, rows: 2, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm" %>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div>
|
|
31
|
+
<%= form.label :effective_at, class: "block text-sm font-medium text-gray-700" %>
|
|
32
|
+
<%= form.datetime_local_field :effective_at, class: "mt-1 block w-full rounded-md border-gray-300 shadow-sm" %>
|
|
33
|
+
<p class="mt-1 text-sm text-gray-500">Leave blank to save as draft.</p>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div>
|
|
37
|
+
<%= form.label :content, class: "block text-sm font-medium text-gray-700" %>
|
|
38
|
+
<%= form.rich_text_area :content, class: "mt-1 block w-full" %>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div>
|
|
42
|
+
<%= form.submit class: "btn btn-primary" %>
|
|
43
|
+
</div>
|
|
44
|
+
<% end %>
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
<div class="mx-auto max-w-6xl px-4 py-8">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold">Legal Documents</h1>
|
|
4
|
+
<%= link_to "New Document", agreement_engine.new_admin_document_path, class: "btn btn-primary" %>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="mb-4 flex gap-2">
|
|
8
|
+
<%= link_to "All", agreement_engine.admin_documents_path, class: "px-3 py-1 rounded #{params[:scope].blank? ? 'bg-gray-900 text-white' : 'bg-gray-100'}" %>
|
|
9
|
+
<%= link_to "Published", agreement_engine.admin_documents_path(scope: "published"), class: "px-3 py-1 rounded #{params[:scope] == 'published' ? 'bg-gray-900 text-white' : 'bg-gray-100'}" %>
|
|
10
|
+
<%= link_to "Draft", agreement_engine.admin_documents_path(scope: "draft"), class: "px-3 py-1 rounded #{params[:scope] == 'draft' ? 'bg-gray-900 text-white' : 'bg-gray-100'}" %>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500">Type</th>
|
|
17
|
+
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500">Version</th>
|
|
18
|
+
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500">Status</th>
|
|
19
|
+
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500">Effective</th>
|
|
20
|
+
<th class="px-4 py-2 text-left text-sm font-medium text-gray-500">Actions</th>
|
|
21
|
+
</tr>
|
|
22
|
+
</thead>
|
|
23
|
+
<tbody class="divide-y divide-gray-200">
|
|
24
|
+
<% @documents.each do |document| %>
|
|
25
|
+
<tr>
|
|
26
|
+
<td class="px-4 py-2"><%= document.document_type.titleize %></td>
|
|
27
|
+
<td class="px-4 py-2"><%= document.version %></td>
|
|
28
|
+
<td class="px-4 py-2"><%= document.published? ? "Published" : "Draft" %></td>
|
|
29
|
+
<td class="px-4 py-2"><%= document.effective_at&.strftime("%B %d, %Y") || "—" %></td>
|
|
30
|
+
<td class="px-4 py-2 flex gap-2">
|
|
31
|
+
<%= link_to "View", agreement_engine.admin_document_path(document) %>
|
|
32
|
+
<%= link_to "Edit", agreement_engine.edit_admin_document_path(document) %>
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
<% end %>
|
|
36
|
+
</tbody>
|
|
37
|
+
</table>
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div class="mx-auto max-w-4xl px-4 py-8">
|
|
2
|
+
<div class="flex items-center justify-between mb-6">
|
|
3
|
+
<h1 class="text-2xl font-bold"><%= @document.document_type.titleize %> v<%= @document.version %></h1>
|
|
4
|
+
<div class="flex gap-2">
|
|
5
|
+
<%= link_to "Edit", agreement_engine.edit_admin_document_path(@document), class: "btn btn-secondary" %>
|
|
6
|
+
<%= link_to "Back", agreement_engine.admin_documents_path, class: "btn btn-secondary" %>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<dl class="grid grid-cols-2 gap-4 mb-6">
|
|
11
|
+
<div>
|
|
12
|
+
<dt class="text-sm font-medium text-gray-500">Status</dt>
|
|
13
|
+
<dd><%= @document.published? ? "Published" : "Draft" %></dd>
|
|
14
|
+
</div>
|
|
15
|
+
<div>
|
|
16
|
+
<dt class="text-sm font-medium text-gray-500">Effective Date</dt>
|
|
17
|
+
<dd><%= @document.effective_at&.strftime("%B %d, %Y") || "Not set" %></dd>
|
|
18
|
+
</div>
|
|
19
|
+
<% if @document.summary.present? %>
|
|
20
|
+
<div class="col-span-2">
|
|
21
|
+
<dt class="text-sm font-medium text-gray-500">Summary</dt>
|
|
22
|
+
<dd><%= @document.summary %></dd>
|
|
23
|
+
</div>
|
|
24
|
+
<% end %>
|
|
25
|
+
</dl>
|
|
26
|
+
|
|
27
|
+
<article class="prose prose-gray dark:prose-invert max-w-none">
|
|
28
|
+
<%= @document.content %>
|
|
29
|
+
</article>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div class="max-w-4xl mx-auto p-4 my-8 lg:my-24">
|
|
2
|
+
<h1 class="mb-4"><%= t(".title", agreement: @agreement.title) %></h1>
|
|
3
|
+
|
|
4
|
+
<% if @version %>
|
|
5
|
+
<%= tag.p t(".last_updated", date: l(@version.effective_at.to_date, format: :long)), class: "mb-4 text-gray-700 leading-normal italic" %>
|
|
6
|
+
|
|
7
|
+
<article class="prose prose-gray dark:prose-invert max-w-none">
|
|
8
|
+
<%= @version.content %>
|
|
9
|
+
</article>
|
|
10
|
+
<% else %>
|
|
11
|
+
<p class="text-gray-500">No version has been published yet.</p>
|
|
12
|
+
<% end %>
|
|
13
|
+
|
|
14
|
+
<div class="mt-8 sm:mt-16">
|
|
15
|
+
<div class="text-center">
|
|
16
|
+
<%= t(".description", agreement: @agreement.title) %>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="mt-4 flex items-center justify-center gap-2">
|
|
20
|
+
<%= button_to t(".decline"), agreement_engine.agreement_path(@agreement), method: :delete, class: "btn btn-secondary btn-large" %>
|
|
21
|
+
<%= button_to t(".accept"), agreement_engine.agreement_path(@agreement), method: :patch, class: "btn btn-primary btn-large" %>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% content_for :title, "Privacy Policy" %>
|
|
2
|
+
|
|
3
|
+
<div class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
|
|
4
|
+
<article class="prose prose-gray dark:prose-invert max-w-none">
|
|
5
|
+
<h1>Privacy Policy</h1>
|
|
6
|
+
|
|
7
|
+
<% if @version %>
|
|
8
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
9
|
+
Version <%= @version.version %> - Effective <%= @version.effective_at.strftime("%B %d, %Y") %>
|
|
10
|
+
</p>
|
|
11
|
+
<%= @version.content %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<p class="text-gray-500">No privacy policy has been published yet.</p>
|
|
14
|
+
<% end %>
|
|
15
|
+
</article>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% content_for :title, "Terms of Service" %>
|
|
2
|
+
|
|
3
|
+
<div class="mx-auto max-w-4xl px-4 py-12 sm:px-6 lg:px-8">
|
|
4
|
+
<article class="prose prose-gray dark:prose-invert max-w-none">
|
|
5
|
+
<h1>Terms of Service</h1>
|
|
6
|
+
|
|
7
|
+
<% if @version %>
|
|
8
|
+
<p class="text-sm text-gray-500 dark:text-gray-400">
|
|
9
|
+
Version <%= @version.version %> - Effective <%= @version.effective_at.strftime("%B %d, %Y") %>
|
|
10
|
+
</p>
|
|
11
|
+
<%= @version.content %>
|
|
12
|
+
<% else %>
|
|
13
|
+
<p class="text-gray-500">No terms of service have been published yet.</p>
|
|
14
|
+
<% end %>
|
|
15
|
+
</article>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
en:
|
|
2
|
+
agreement_engine:
|
|
3
|
+
agreements:
|
|
4
|
+
show:
|
|
5
|
+
title: "%{agreement} Changed"
|
|
6
|
+
last_updated: "Last updated %{date}"
|
|
7
|
+
description: "Before you can proceed, you must read and accept the new %{agreement}."
|
|
8
|
+
accept: "I Accept"
|
|
9
|
+
decline: "I Decline"
|
|
10
|
+
destroy:
|
|
11
|
+
declined: "To continue using the service, please sign in and accept the %{agreement}."
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
AgreementEngine::Engine.routes.draw do
|
|
2
|
+
resources :agreements, only: [:show, :update, :destroy]
|
|
3
|
+
|
|
4
|
+
get "terms", to: "documents#terms", as: :terms
|
|
5
|
+
get "privacy", to: "documents#privacy", as: :privacy
|
|
6
|
+
|
|
7
|
+
namespace :admin do
|
|
8
|
+
resources :documents
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
class CreateFinePrintDocuments < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :fine_print_documents do |t|
|
|
4
|
+
t.string :document_type, null: false
|
|
5
|
+
t.string :version, null: false
|
|
6
|
+
t.datetime :effective_at
|
|
7
|
+
t.text :summary
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
add_index :fine_print_documents, [:document_type, :version], unique: true
|
|
13
|
+
add_index :fine_print_documents, [:document_type, :effective_at]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
class AddFinePrintToUsers < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
add_column :users, :accepted_terms_of_service_version_id, :bigint
|
|
4
|
+
add_column :users, :accepted_privacy_policy_version_id, :bigint
|
|
5
|
+
add_foreign_key :users, :fine_print_documents, column: :accepted_terms_of_service_version_id
|
|
6
|
+
add_foreign_key :users, :fine_print_documents, column: :accepted_privacy_policy_version_id
|
|
7
|
+
end
|
|
8
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
Agreement = Data.define(:id, :title, :version_column, :document_type, :prompt_when_updated) do
|
|
5
|
+
def current_version
|
|
6
|
+
AgreementEngine::Document.current(document_type)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def accepted_by?(user)
|
|
10
|
+
current = current_version
|
|
11
|
+
return true unless current
|
|
12
|
+
user.public_send(version_column) == current.id
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def not_accepted_by?(user)
|
|
16
|
+
!accepted_by?(user)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def to_param = id
|
|
20
|
+
|
|
21
|
+
def to_partial_path = "agreement_engine/agreements/#{id}"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :agreements
|
|
6
|
+
attr_accessor :current_user_method
|
|
7
|
+
attr_accessor :signed_in_method
|
|
8
|
+
attr_accessor :auth_controller_method
|
|
9
|
+
attr_accessor :sign_out_method
|
|
10
|
+
attr_accessor :admin_auth_method
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@agreements = []
|
|
14
|
+
|
|
15
|
+
@current_user_method = ->(c) { c.current_user }
|
|
16
|
+
@signed_in_method = ->(c) { c.user_signed_in? }
|
|
17
|
+
@auth_controller_method = ->(c) { c.respond_to?(:devise_controller?) && c.devise_controller? }
|
|
18
|
+
@sign_out_method = ->(c) {
|
|
19
|
+
if defined?(Devise) && Devise.sign_out_all_scopes
|
|
20
|
+
c.sign_out
|
|
21
|
+
else
|
|
22
|
+
c.sign_out(c.current_user)
|
|
23
|
+
end
|
|
24
|
+
}
|
|
25
|
+
@admin_auth_method = ->(c) {
|
|
26
|
+
c.redirect_to("/", alert: "Not authorized") unless signed_in?(c)
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def current_user(controller) = current_user_method.call(controller)
|
|
31
|
+
def signed_in?(controller) = signed_in_method.call(controller)
|
|
32
|
+
def auth_controller?(controller) = auth_controller_method.call(controller)
|
|
33
|
+
def sign_out(controller) = sign_out_method.call(controller)
|
|
34
|
+
def authenticate_admin!(controller) = admin_auth_method.call(controller)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
module Enforceable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :require_accepted_agreements!,
|
|
9
|
+
if: -> { request.get? && AgreementEngine.config.signed_in?(self) && !AgreementEngine.config.auth_controller?(self) }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def require_accepted_agreements!
|
|
13
|
+
AgreementEngine.config.agreements.select(&:prompt_when_updated).each do |agreement|
|
|
14
|
+
if agreement.not_accepted_by?(AgreementEngine.config.current_user(self))
|
|
15
|
+
store_location_for(:user, request.fullpath) unless request.fullpath.start_with?("/agreements/")
|
|
16
|
+
redirect_to agreement_engine.agreement_path(agreement)
|
|
17
|
+
break
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace AgreementEngine
|
|
6
|
+
|
|
7
|
+
initializer "agreement_engine.url_helpers" do
|
|
8
|
+
ActiveSupport.on_load(:action_controller) do
|
|
9
|
+
helper Rails.application.routes.url_helpers
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AgreementEngine
|
|
4
|
+
module Signable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
AgreementEngine.config.agreements.each do |agreement|
|
|
9
|
+
belongs_to :"accepted_#{agreement.document_type}_version",
|
|
10
|
+
class_name: "AgreementEngine::Document",
|
|
11
|
+
optional: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def needs_acceptance?(agreement)
|
|
16
|
+
agreement.not_accepted_by?(self)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def accept_version!(agreement, version)
|
|
20
|
+
update!(agreement.version_column => version.id)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "agreement_engine/version"
|
|
4
|
+
require_relative "agreement_engine/configuration"
|
|
5
|
+
require_relative "agreement_engine/agreement"
|
|
6
|
+
|
|
7
|
+
module AgreementEngine
|
|
8
|
+
class << self
|
|
9
|
+
def config
|
|
10
|
+
@config ||= Configuration.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def configure
|
|
14
|
+
yield config
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative "agreement_engine/engine"
|
|
20
|
+
require_relative "agreement_engine/signable"
|
|
21
|
+
require_relative "agreement_engine/enforceable"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators"
|
|
4
|
+
require "rails/generators/active_record"
|
|
5
|
+
|
|
6
|
+
module AgreementEngine
|
|
7
|
+
module Generators
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
include ActiveRecord::Generators::Migration
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Installs AgreementEngine: adds route, copies initializer and migrations."
|
|
14
|
+
|
|
15
|
+
def mount_engine
|
|
16
|
+
route 'mount AgreementEngine::Engine, at: "/legal"'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def copy_initializer
|
|
20
|
+
template "initializer.rb", "config/initializers/agreement_engine.rb"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def copy_migrations
|
|
24
|
+
migrations_dir = AgreementEngine::Engine.root.join("db/migrate")
|
|
25
|
+
Dir[migrations_dir.join("*.rb")].sort.each do |migration|
|
|
26
|
+
filename = File.basename(migration).sub(/^\d+_/, "")
|
|
27
|
+
migration_template migration, "db/migrate/#{filename}"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
AgreementEngine.configure do |config|
|
|
2
|
+
# Lambda called to get the current user (default: controller.current_user)
|
|
3
|
+
# config.current_user_method = ->(controller) { controller.current_user }
|
|
4
|
+
|
|
5
|
+
# Lambda called to check if a user is signed in (default: controller.user_signed_in?)
|
|
6
|
+
# config.signed_in_method = ->(controller) { controller.user_signed_in? }
|
|
7
|
+
|
|
8
|
+
# Lambda called to authorize admin access (default: redirects if not signed in)
|
|
9
|
+
# config.admin_auth_method = ->(controller) {
|
|
10
|
+
# controller.redirect_to("/", alert: "Not authorized") unless config.signed_in?(controller)
|
|
11
|
+
# }
|
|
12
|
+
|
|
13
|
+
# Define your legal agreements:
|
|
14
|
+
# config.agreements = [
|
|
15
|
+
# AgreementEngine::Agreement.new(:terms, title: "Terms of Service", required: true),
|
|
16
|
+
# AgreementEngine::Agreement.new(:privacy, title: "Privacy Policy", required: true),
|
|
17
|
+
# ]
|
|
18
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
namespace :agreement_engine do
|
|
2
|
+
desc "Seed initial legal document versions for each configured agreement"
|
|
3
|
+
task seed: :environment do
|
|
4
|
+
puts "Seeding legal documents..."
|
|
5
|
+
|
|
6
|
+
AgreementEngine.config.agreements.each do |agreement|
|
|
7
|
+
if AgreementEngine::Document.where(document_type: agreement.document_type).exists?
|
|
8
|
+
puts " #{agreement.title} v1.0 already exists, skipping"
|
|
9
|
+
else
|
|
10
|
+
AgreementEngine::Document.create!(
|
|
11
|
+
document_type: agreement.document_type,
|
|
12
|
+
version: "1.0",
|
|
13
|
+
summary: "Initial #{agreement.title.downcase}",
|
|
14
|
+
content: "<h2>#{agreement.title}</h2><p>This is a placeholder. Please update with your actual #{agreement.title.downcase}.</p>",
|
|
15
|
+
effective_at: Time.current
|
|
16
|
+
)
|
|
17
|
+
puts " Created #{agreement.title} v1.0"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
puts "Done!"
|
|
22
|
+
end
|
|
23
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: agreement_engine
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- tylercschneider
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "<"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "<"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rails
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '7.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '7.1'
|
|
40
|
+
description: Track and enforce user acceptance of Terms of Service, Privacy Policy,
|
|
41
|
+
and other legal agreements with versioned documents and audit trails.
|
|
42
|
+
email:
|
|
43
|
+
- tylercschneider@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- ".github/workflows/ci.yml"
|
|
49
|
+
- ".github/workflows/release.yml"
|
|
50
|
+
- ".ruby-version"
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- LICENSE
|
|
53
|
+
- README.md
|
|
54
|
+
- Rakefile
|
|
55
|
+
- app/controllers/agreement_engine/admin/documents_controller.rb
|
|
56
|
+
- app/controllers/agreement_engine/agreements_controller.rb
|
|
57
|
+
- app/controllers/agreement_engine/documents_controller.rb
|
|
58
|
+
- app/models/agreement_engine/document.rb
|
|
59
|
+
- app/views/agreement_engine/admin/documents/_form.html.erb
|
|
60
|
+
- app/views/agreement_engine/admin/documents/edit.html.erb
|
|
61
|
+
- app/views/agreement_engine/admin/documents/index.html.erb
|
|
62
|
+
- app/views/agreement_engine/admin/documents/new.html.erb
|
|
63
|
+
- app/views/agreement_engine/admin/documents/show.html.erb
|
|
64
|
+
- app/views/agreement_engine/agreements/show.html.erb
|
|
65
|
+
- app/views/agreement_engine/documents/privacy.html.erb
|
|
66
|
+
- app/views/agreement_engine/documents/terms.html.erb
|
|
67
|
+
- app/views/layouts/agreement_engine/minimal.html.erb
|
|
68
|
+
- config/locales/en.yml
|
|
69
|
+
- config/routes.rb
|
|
70
|
+
- db/migrate/20260208000001_create_fine_print_documents.rb
|
|
71
|
+
- db/migrate/20260208000002_add_fine_print_to_users.rb
|
|
72
|
+
- db/migrate/20260921200000_rename_fine_print_tables_to_agreement_engine.rb
|
|
73
|
+
- lib/agreement_engine.rb
|
|
74
|
+
- lib/agreement_engine/agreement.rb
|
|
75
|
+
- lib/agreement_engine/configuration.rb
|
|
76
|
+
- lib/agreement_engine/enforceable.rb
|
|
77
|
+
- lib/agreement_engine/engine.rb
|
|
78
|
+
- lib/agreement_engine/signable.rb
|
|
79
|
+
- lib/agreement_engine/version.rb
|
|
80
|
+
- lib/generators/agreement_engine/install_generator.rb
|
|
81
|
+
- lib/generators/agreement_engine/templates/initializer.rb
|
|
82
|
+
- lib/tasks/agreement_engine.rake
|
|
83
|
+
homepage: https://github.com/DYB-Development/agreement_engine
|
|
84
|
+
licenses:
|
|
85
|
+
- MIT
|
|
86
|
+
metadata:
|
|
87
|
+
homepage_uri: https://github.com/DYB-Development/agreement_engine
|
|
88
|
+
source_code_uri: https://github.com/DYB-Development/agreement_engine
|
|
89
|
+
rdoc_options: []
|
|
90
|
+
require_paths:
|
|
91
|
+
- lib
|
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: 3.2.0
|
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
requirements: []
|
|
103
|
+
rubygems_version: 4.0.20
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: Versioned legal document management for Rails
|
|
106
|
+
test_files: []
|