open-loam 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/CHANGELOG.md +71 -0
- data/LICENSE +21 -0
- data/README.md +177 -0
- data/app/jobs/loam/custom_field_reindex_job.rb +22 -0
- data/app/jobs/loam/event_delivery_job.rb +26 -0
- data/app/jobs/loam/event_redelivery_sweep_job.rb +21 -0
- data/app/jobs/loam/webhook_delivery_job.rb +59 -0
- data/app/models/loam/api_token.rb +40 -0
- data/app/models/loam/audit_record.rb +18 -0
- data/app/models/loam/auth_attempt.rb +13 -0
- data/app/models/loam/business_rule.rb +49 -0
- data/app/models/loam/business_rule_run.rb +27 -0
- data/app/models/loam/comment.rb +28 -0
- data/app/models/loam/config.rb +27 -0
- data/app/models/loam/custom_field_value.rb +18 -0
- data/app/models/loam/dashboard_widget.rb +16 -0
- data/app/models/loam/dictionary.rb +20 -0
- data/app/models/loam/dictionary_entry.rb +31 -0
- data/app/models/loam/event_delivery.rb +38 -0
- data/app/models/loam/field_definition.rb +48 -0
- data/app/models/loam/inbound_webhook_delivery.rb +24 -0
- data/app/models/loam/inbound_webhook_source.rb +55 -0
- data/app/models/loam/membership.rb +27 -0
- data/app/models/loam/mfa_credential.rb +114 -0
- data/app/models/loam/notification.rb +36 -0
- data/app/models/loam/pending_action.rb +208 -0
- data/app/models/loam/perspective.rb +104 -0
- data/app/models/loam/progress_job.rb +103 -0
- data/app/models/loam/record_lock.rb +19 -0
- data/app/models/loam/scheduled_job.rb +42 -0
- data/app/models/loam/search_token.rb +9 -0
- data/app/models/loam/sso_identity.rb +13 -0
- data/app/models/loam/sso_provider.rb +35 -0
- data/app/models/loam/tenant.rb +20 -0
- data/app/models/loam/translation.rb +17 -0
- data/app/models/loam/webhook_endpoint.rb +26 -0
- data/app/views/loam/attachments/_attachments.html.erb +16 -0
- data/app/views/loam/comments/_comments.html.erb +24 -0
- data/app/views/loam/custom_fields/_fields.html.erb +35 -0
- data/lib/generators/loam/entity/entity_generator.rb +160 -0
- data/lib/generators/loam/entity/templates/api_controller.rb +67 -0
- data/lib/generators/loam/entity/templates/controller.rb +181 -0
- data/lib/generators/loam/entity/templates/entity_test.rb +107 -0
- data/lib/generators/loam/entity/templates/migration.rb +26 -0
- data/lib/generators/loam/entity/templates/model.rb +27 -0
- data/lib/generators/loam/entity/templates/policy.rb +10 -0
- data/lib/generators/loam/entity/templates/views/_form.html.erb +63 -0
- data/lib/generators/loam/entity/templates/views/deleted.html.erb +41 -0
- data/lib/generators/loam/entity/templates/views/edit.html.erb +3 -0
- data/lib/generators/loam/entity/templates/views/index.html.erb +90 -0
- data/lib/generators/loam/entity/templates/views/new.html.erb +3 -0
- data/lib/generators/loam/entity/templates/views/show.html.erb +36 -0
- data/lib/generators/loam/install/install_generator.rb +288 -0
- data/lib/generators/loam/install/templates/AGENTS.md +341 -0
- data/lib/generators/loam/install/templates/admin/api_docs_controller.rb +17 -0
- data/lib/generators/loam/install/templates/admin/api_docs_index.html.erb +34 -0
- data/lib/generators/loam/install/templates/admin/api_tokens_controller.rb +30 -0
- data/lib/generators/loam/install/templates/admin/api_tokens_index.html.erb +39 -0
- data/lib/generators/loam/install/templates/admin/base_controller.rb +224 -0
- data/lib/generators/loam/install/templates/admin/business_rules_controller.rb +64 -0
- data/lib/generators/loam/install/templates/admin/business_rules_edit.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/business_rules_form.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/business_rules_index.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/business_rules_new.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/comments_controller.rb +39 -0
- data/lib/generators/loam/install/templates/admin/configs_controller.rb +46 -0
- data/lib/generators/loam/install/templates/admin/configs_edit.html.erb +22 -0
- data/lib/generators/loam/install/templates/admin/configs_index.html.erb +30 -0
- data/lib/generators/loam/install/templates/admin/dashboard_controller.rb +10 -0
- data/lib/generators/loam/install/templates/admin/dashboard_index.html.erb +26 -0
- data/lib/generators/loam/install/templates/admin/dashboard_widgets_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/dashboard_widgets_index.html.erb +24 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_controller.rb +55 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_edit.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_form.html.erb +10 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_index.html.erb +21 -0
- data/lib/generators/loam/install/templates/admin/dictionaries_new.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/dictionary_entries_controller.rb +47 -0
- data/lib/generators/loam/install/templates/admin/event_deliveries_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/event_deliveries_index.html.erb +49 -0
- data/lib/generators/loam/install/templates/admin/events_controller.rb +42 -0
- data/lib/generators/loam/install/templates/admin/features_controller.rb +35 -0
- data/lib/generators/loam/install/templates/admin/features_index.html.erb +37 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_controller.rb +52 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_index.html.erb +30 -0
- data/lib/generators/loam/install/templates/admin/field_definitions_new.html.erb +39 -0
- data/lib/generators/loam/install/templates/admin/history_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/history_index.html.erb +35 -0
- data/lib/generators/loam/install/templates/admin/imports_controller.rb +75 -0
- data/lib/generators/loam/install/templates/admin/imports_new.html.erb +10 -0
- data/lib/generators/loam/install/templates/admin/imports_preview.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/imports_summary.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_controller.rb +61 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_index.html.erb +49 -0
- data/lib/generators/loam/install/templates/admin/inbound_webhook_sources_new.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/layout.html.erb +76 -0
- data/lib/generators/loam/install/templates/admin/mfa_activated.html.erb +12 -0
- data/lib/generators/loam/install/templates/admin/mfa_controller.rb +61 -0
- data/lib/generators/loam/install/templates/admin/mfa_new.html.erb +23 -0
- data/lib/generators/loam/install/templates/admin/mfa_show.html.erb +19 -0
- data/lib/generators/loam/install/templates/admin/notifications_controller.rb +23 -0
- data/lib/generators/loam/install/templates/admin/notifications_index.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/overrides_controller.rb +14 -0
- data/lib/generators/loam/install/templates/admin/overrides_index.html.erb +29 -0
- data/lib/generators/loam/install/templates/admin/pagination.rb +23 -0
- data/lib/generators/loam/install/templates/admin/pending_actions_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/pending_actions_index.html.erb +40 -0
- data/lib/generators/loam/install/templates/admin/perspectives_controller.rb +98 -0
- data/lib/generators/loam/install/templates/admin/perspectives_index.html.erb +38 -0
- data/lib/generators/loam/install/templates/admin/progress_jobs_controller.rb +19 -0
- data/lib/generators/loam/install/templates/admin/progress_jobs_index.html.erb +31 -0
- data/lib/generators/loam/install/templates/admin/record_locks_controller.rb +17 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_controller.rb +69 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_edit.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_form.html.erb +16 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_index.html.erb +28 -0
- data/lib/generators/loam/install/templates/admin/scheduled_jobs_new.html.erb +5 -0
- data/lib/generators/loam/install/templates/admin/search_controller.rb +49 -0
- data/lib/generators/loam/install/templates/admin/search_index.html.erb +23 -0
- data/lib/generators/loam/install/templates/admin/sessions_controller.rb +248 -0
- data/lib/generators/loam/install/templates/admin/sessions_mfa_challenge.html.erb +18 -0
- data/lib/generators/loam/install/templates/admin/sessions_new.html.erb +45 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_controller.rb +65 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_edit.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_form.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_index.html.erb +26 -0
- data/lib/generators/loam/install/templates/admin/sso_providers_new.html.erb +3 -0
- data/lib/generators/loam/install/templates/admin/sudo_controller.rb +45 -0
- data/lib/generators/loam/install/templates/admin/sudo_new.html.erb +24 -0
- data/lib/generators/loam/install/templates/admin/translations_controller.rb +37 -0
- data/lib/generators/loam/install/templates/admin/translations_index.html.erb +21 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_controller.rb +38 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_index.html.erb +32 -0
- data/lib/generators/loam/install/templates/admin/webhook_endpoints_new.html.erb +28 -0
- data/lib/generators/loam/install/templates/admin.css +29 -0
- data/lib/generators/loam/install/templates/api_base_controller.rb +90 -0
- data/lib/generators/loam/install/templates/guardrails_test.rb +79 -0
- data/lib/generators/loam/install/templates/import_job.rb +25 -0
- data/lib/generators/loam/install/templates/inbound_webhooks_controller.rb +17 -0
- data/lib/generators/loam/install/templates/initializer.rb +218 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_api_tokens.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_audit_records.rb +14 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_auth_attempts.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_business_rules.rb +29 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_comments.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_configs.rb +21 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_custom_field_values.rb +19 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dashboard_widgets.rb +12 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionaries.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_dictionary_entries.rb +18 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_event_deliveries.rb +17 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_field_definitions.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_inbound_webhooks.rb +31 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_memberships.rb +11 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_mfa_credentials.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_notifications.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_pending_actions.rb +26 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_perspectives.rb +17 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_progress_jobs.rb +20 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_record_locks.rb +16 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_scheduled_jobs.rb +20 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_search_tokens.rb +18 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_sso_providers.rb +34 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_tenants.rb +9 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_translations.rb +15 -0
- data/lib/generators/loam/install/templates/migrations/create_loam_webhook_endpoints.rb +13 -0
- data/lib/generators/loam/install/templates/migrations/create_users.rb +13 -0
- data/lib/generators/loam/install/templates/user.rb +21 -0
- data/lib/generators/loam/primary_key_options.rb +101 -0
- data/lib/loam/attachable.rb +27 -0
- data/lib/loam/auditable.rb +67 -0
- data/lib/loam/auth_throttle.rb +83 -0
- data/lib/loam/base32.rb +23 -0
- data/lib/loam/bulk.rb +59 -0
- data/lib/loam/business_rules/actions.rb +92 -0
- data/lib/loam/business_rules/condition.rb +102 -0
- data/lib/loam/business_rules.rb +123 -0
- data/lib/loam/commentable.rb +22 -0
- data/lib/loam/configs.rb +115 -0
- data/lib/loam/cron.rb +98 -0
- data/lib/loam/csv.rb +18 -0
- data/lib/loam/current.rb +21 -0
- data/lib/loam/custom_field_index.rb +262 -0
- data/lib/loam/custom_fields.rb +114 -0
- data/lib/loam/dashboard.rb +17 -0
- data/lib/loam/dictionaries.rb +55 -0
- data/lib/loam/durable_events.rb +174 -0
- data/lib/loam/encryptable.rb +145 -0
- data/lib/loam/encryption/cipher.rb +75 -0
- data/lib/loam/encryption/key_provider.rb +43 -0
- data/lib/loam/encryption.rb +125 -0
- data/lib/loam/engine.rb +57 -0
- data/lib/loam/enrichers.rb +105 -0
- data/lib/loam/errors.rb +49 -0
- data/lib/loam/eval.rb +47 -0
- data/lib/loam/event_stream.rb +80 -0
- data/lib/loam/eventful.rb +45 -0
- data/lib/loam/events.rb +51 -0
- data/lib/loam/export.rb +74 -0
- data/lib/loam/features.rb +75 -0
- data/lib/loam/generated_key.rb +37 -0
- data/lib/loam/import.rb +168 -0
- data/lib/loam/inbound_webhooks.rb +125 -0
- data/lib/loam/lifecycle.rb +180 -0
- data/lib/loam/locales/loam.en.yml +80 -0
- data/lib/loam/mcp/server.rb +60 -0
- data/lib/loam/mcp.rb +304 -0
- data/lib/loam/notifications.rb +36 -0
- data/lib/loam/open_api.rb +194 -0
- data/lib/loam/overrides.rb +110 -0
- data/lib/loam/pending_actions.rb +60 -0
- data/lib/loam/permissions.rb +79 -0
- data/lib/loam/perspectives.rb +62 -0
- data/lib/loam/policy.rb +106 -0
- data/lib/loam/progress.rb +31 -0
- data/lib/loam/record_locks.rb +89 -0
- data/lib/loam/scheduler.rb +153 -0
- data/lib/loam/search/driver.rb +31 -0
- data/lib/loam/search/like_driver.rb +29 -0
- data/lib/loam/search/token_driver.rb +88 -0
- data/lib/loam/search.rb +27 -0
- data/lib/loam/searchable.rb +90 -0
- data/lib/loam/soft_deletable.rb +72 -0
- data/lib/loam/sso/claims.rb +10 -0
- data/lib/loam/sso/fake_provider.rb +51 -0
- data/lib/loam/sso/http_client.rb +42 -0
- data/lib/loam/sso/oidc_provider.rb +64 -0
- data/lib/loam/sso.rb +129 -0
- data/lib/loam/telemetry.rb +43 -0
- data/lib/loam/tenant_record.rb +36 -0
- data/lib/loam/test_helpers.rb +15 -0
- data/lib/loam/totp.rb +63 -0
- data/lib/loam/translatable.rb +88 -0
- data/lib/loam/undo.rb +126 -0
- data/lib/loam/version.rb +3 -0
- data/lib/loam/webhooks.rb +34 -0
- data/lib/loam/widgets.rb +77 -0
- data/lib/loam/workflow.rb +256 -0
- data/lib/loam.rb +122 -0
- data/lib/open-loam.rb +5 -0
- data/lib/tasks/loam.rake +171 -0
- data/lib/tasks/loam_eval.rake +32 -0
- data/lib/tasks/loam_mcp.rake +20 -0
- metadata +332 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module Api
|
|
2
|
+
# JSON for <%= class_name %>. Same rules as the admin screen, same policy:
|
|
3
|
+
# the token's user is the actor, so a role that may not write a field cannot
|
|
4
|
+
# write it here either — `permitted_fields` drops it from the permit list.
|
|
5
|
+
class <%= plural_name.camelize %>Controller < BaseController
|
|
6
|
+
FIELDS = %i[<%= field_names.join(" ") %>].freeze
|
|
7
|
+
|
|
8
|
+
before_action :set_record, only: %i[show update destroy]
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
records = <%= class_name %>.order(created_at: :desc).to_a
|
|
12
|
+
enriched = Loam::Enrichers.enrich_many(records) # one batched pass, no N+1
|
|
13
|
+
render json: records.map { |record| entity_json(record, enrichments: enriched[record.id]) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
authorize!(policy_for(@record), :read?)
|
|
18
|
+
render json: entity_json(@record)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create
|
|
22
|
+
@record = <%= class_name %>.new
|
|
23
|
+
policy = policy_for(@record)
|
|
24
|
+
authorize!(policy, :create?)
|
|
25
|
+
@record.assign_attributes(permitted_params(policy))
|
|
26
|
+
assign_custom_fields!(@record, params, policy)
|
|
27
|
+
|
|
28
|
+
if @record.save
|
|
29
|
+
render json: entity_json(@record), status: :created
|
|
30
|
+
else
|
|
31
|
+
render json: { errors: @record.errors.full_messages }, status: :unprocessable_entity
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def update
|
|
36
|
+
policy = policy_for(@record)
|
|
37
|
+
authorize!(policy, :update?)
|
|
38
|
+
@record.assign_attributes(permitted_params(policy))
|
|
39
|
+
assign_custom_fields!(@record, params, policy)
|
|
40
|
+
|
|
41
|
+
if @record.save
|
|
42
|
+
render json: entity_json(@record)
|
|
43
|
+
else
|
|
44
|
+
render json: { errors: @record.errors.full_messages }, status: :unprocessable_entity
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Soft-delete here too, so DELETE is the ONE way to delete across both
|
|
49
|
+
# surfaces: the record is hidden and recoverable, not erased. The recycle
|
|
50
|
+
# bin itself is an admin concern; the API just gets the safer default.
|
|
51
|
+
def destroy
|
|
52
|
+
authorize!(policy_for(@record), :destroy?)
|
|
53
|
+
@record.soft_delete!
|
|
54
|
+
head :no_content
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def set_record
|
|
60
|
+
@record = <%= class_name %>.find(params[:id])
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def permitted_params(policy)
|
|
64
|
+
params.require(:<%= singular_name %>).permit(*policy.permitted_fields(FIELDS))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
module Admin
|
|
2
|
+
class <%= plural_name.camelize %>Controller < BaseController
|
|
3
|
+
FIELDS = %i[<%= field_names.join(" ") %>].freeze
|
|
4
|
+
|
|
5
|
+
before_action :set_record, only: %i[show edit update destroy]
|
|
6
|
+
|
|
7
|
+
helper_method :current_index_params
|
|
8
|
+
|
|
9
|
+
# The active index filters (search, saved view, custom-field filter, sort) as
|
|
10
|
+
# a plain hash, so pagination links, the sort headers and the export link all
|
|
11
|
+
# carry the SAME state — page 2 of a filtered/sorted view stays filtered.
|
|
12
|
+
def current_index_params
|
|
13
|
+
params.permit(:q, :perspective_id, :cf_field, :cf_op, :cf_value, :sort, :dir)
|
|
14
|
+
.to_h.symbolize_keys.reject { |_, v| v.blank? }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def index
|
|
18
|
+
@perspective = Loam::Perspectives.resolve("<%= class_name %>", user: current_actor, id: params[:perspective_id])
|
|
19
|
+
@perspectives = Loam::Perspectives.visible_to("<%= class_name %>", user: current_actor)
|
|
20
|
+
@records, @page, @has_next = paginate(index_scope)
|
|
21
|
+
@index_partial = params[:cf_field].present? && Loam::CustomFieldIndex.partial? # incomplete-index warning (L-919)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# CSV of the CURRENT filtered/perspective view — manager-only, policy- and
|
|
25
|
+
# encryption-aware (Loam::Export).
|
|
26
|
+
def export
|
|
27
|
+
require_role!(:manager)
|
|
28
|
+
send_data Loam::Export.csv(index_scope, actor: current_actor),
|
|
29
|
+
filename: "<%= plural_name %>-#{Date.current}.csv", type: "text/csv"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Datatable bulk actions on the selected ids — each is policy-checked per
|
|
33
|
+
# record and tenant-scoped (Loam::Bulk). Zero selection is a no-op.
|
|
34
|
+
def bulk
|
|
35
|
+
ids = Array(params[:ids])
|
|
36
|
+
case params[:bulk_action]
|
|
37
|
+
when "soft_delete"
|
|
38
|
+
count = Loam::Bulk.soft_delete(<%= class_name %>, ids)
|
|
39
|
+
redirect_to polymorphic_path([:admin, <%= class_name %>]), notice: "Deleted #{count} record(s)."
|
|
40
|
+
when "set_field"
|
|
41
|
+
count = Loam::Bulk.set_field(<%= class_name %>, ids, field: params[:field], value: params[:value])
|
|
42
|
+
redirect_to polymorphic_path([:admin, <%= class_name %>]), notice: "Updated #{count} record(s)."
|
|
43
|
+
when "export"
|
|
44
|
+
require_role!(:manager) # same gate as the dedicated export action
|
|
45
|
+
send_data Loam::Export.csv(Loam::Bulk.selected(<%= class_name %>, ids), actor: current_actor),
|
|
46
|
+
filename: "<%= plural_name %>-selected.csv", type: "text/csv"
|
|
47
|
+
else
|
|
48
|
+
redirect_to polymorphic_path([:admin, <%= class_name %>]), alert: "Unknown bulk action."
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# The recycle bin: only_deleted stays tenant-scoped, so this never shows
|
|
53
|
+
# another tenant's deleted rows.
|
|
54
|
+
def deleted
|
|
55
|
+
authorize!(policy_for(<%= class_name %>.new), :read?)
|
|
56
|
+
scope = <%= class_name %>.only_deleted.order(deleted_at: :desc, id: :desc)
|
|
57
|
+
@records, @page, @has_next = paginate(scope)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def show
|
|
61
|
+
authorize!(policy_for(@record), :read?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def new
|
|
65
|
+
@record = <%= class_name %>.new
|
|
66
|
+
authorize!(policy_for(@record), :create?)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create
|
|
70
|
+
@record = <%= class_name %>.new
|
|
71
|
+
policy = policy_for(@record)
|
|
72
|
+
authorize!(policy, :create?)
|
|
73
|
+
@record.assign_attributes(permitted_params(policy))
|
|
74
|
+
assign_custom_fields!(@record, params, policy)
|
|
75
|
+
attach_files!(@record, policy)
|
|
76
|
+
|
|
77
|
+
if @record.save
|
|
78
|
+
redirect_to [:admin, @record], notice: t("loam.flash.created", name: <%= class_name %>.model_name.human)
|
|
79
|
+
else
|
|
80
|
+
render :new, status: :unprocessable_entity
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def edit
|
|
85
|
+
authorize!(policy_for(@record), :update?)
|
|
86
|
+
# Take the advisory lock (courtesy) or learn who holds it, for the banner.
|
|
87
|
+
@lock = Loam::RecordLocks.acquire(@record, by: current_actor) || Loam::RecordLocks.active_lock(@record)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def update
|
|
91
|
+
policy = policy_for(@record)
|
|
92
|
+
authorize!(policy, :update?)
|
|
93
|
+
assign_custom_fields!(@record, params, policy)
|
|
94
|
+
attach_files!(@record, policy)
|
|
95
|
+
|
|
96
|
+
if @record.update(permitted_params(policy))
|
|
97
|
+
Loam::RecordLocks.release(@record, by: current_actor)
|
|
98
|
+
redirect_to [:admin, @record], notice: t("loam.flash.updated", name: <%= class_name %>.model_name.human)
|
|
99
|
+
else
|
|
100
|
+
render :edit, status: :unprocessable_entity
|
|
101
|
+
end
|
|
102
|
+
rescue ActiveRecord::StaleObjectError
|
|
103
|
+
# Someone saved between open and submit — show the diff, reload fresh, retry.
|
|
104
|
+
stale_conflict!(@record, FIELDS)
|
|
105
|
+
@lock = Loam::RecordLocks.acquire(@record, by: current_actor) || Loam::RecordLocks.active_lock(@record)
|
|
106
|
+
render :edit, status: :conflict
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Delete hides, it does not erase — the button is undoable. Reach for the
|
|
110
|
+
# model's `destroy` only when a row must genuinely leave the database.
|
|
111
|
+
def destroy
|
|
112
|
+
authorize!(policy_for(@record), :destroy?)
|
|
113
|
+
@record.soft_delete!
|
|
114
|
+
Loam::RecordLocks.release(@record, by: current_actor)
|
|
115
|
+
redirect_to [:admin, <%= class_name %>], notice: t("loam.flash.destroyed", name: <%= class_name %>.model_name.human)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Restore looks through the deleted rows — the default scope hides them, so a
|
|
119
|
+
# plain find would 404 on the record we are trying to bring back.
|
|
120
|
+
def restore
|
|
121
|
+
@record = <%= class_name %>.with_deleted.find(params[:id])
|
|
122
|
+
authorize!(policy_for(@record), :update?)
|
|
123
|
+
@record.restore!
|
|
124
|
+
redirect_to [:deleted, :admin, <%= class_name %>], notice: t("loam.flash.restored", name: <%= class_name %>.model_name.human)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def set_record
|
|
130
|
+
@record = <%= class_name %>.find(params[:id])
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# The same filtered/perspective/search scope the index shows — reused by
|
|
134
|
+
# export so the CSV matches what the manager is looking at.
|
|
135
|
+
def index_scope
|
|
136
|
+
scope = <%= class_name %>.all
|
|
137
|
+
perspective = Loam::Perspectives.resolve("<%= class_name %>", user: current_actor, id: params[:perspective_id])
|
|
138
|
+
scope = perspective.apply(scope) if perspective
|
|
139
|
+
<% if searchable_attributes.any? -%>
|
|
140
|
+
scope = scope.search(params[:q])
|
|
141
|
+
<% end -%>
|
|
142
|
+
scope = apply_custom_field_filter(scope)
|
|
143
|
+
apply_sort(scope)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Column sort from the clickable index headers. The column is WHITELISTED
|
|
147
|
+
# against real columns (never interpolated from params — that would be SQL
|
|
148
|
+
# injection), and the direction is constrained to asc/desc. With no valid
|
|
149
|
+
# sort param, a perspective's own order is kept, else a stable default.
|
|
150
|
+
def apply_sort(scope)
|
|
151
|
+
column = params[:sort].to_s
|
|
152
|
+
if <%= class_name %>.column_names.include?(column)
|
|
153
|
+
dir = params[:dir].to_s.casecmp("asc").zero? ? :asc : :desc
|
|
154
|
+
scope.reorder(column => dir).order(id: dir)
|
|
155
|
+
elsif scope.order_values.empty?
|
|
156
|
+
scope.order(created_at: :desc, id: :desc)
|
|
157
|
+
else
|
|
158
|
+
scope
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A custom-field filter routed through the read-model index
|
|
163
|
+
# (Loam::CustomFieldIndex) — index-backed, not a JSON scan. An unknown field
|
|
164
|
+
# is ignored rather than raising.
|
|
165
|
+
def apply_custom_field_filter(scope)
|
|
166
|
+
return scope if params[:cf_field].blank?
|
|
167
|
+
|
|
168
|
+
scope.merge(Loam::CustomFieldIndex.filter(<%= class_name %>, params[:cf_field], params[:cf_op].presence || "eq", params[:cf_value]))
|
|
169
|
+
rescue Loam::Error
|
|
170
|
+
scope
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Field-level enforcement: the permit list comes from the policy, so a
|
|
174
|
+
# role without write access to a field cannot smuggle it in via params.
|
|
175
|
+
# lock_version rides outside the policy filter — it is optimistic-locking
|
|
176
|
+
# plumbing, and a forged value just produces a harmless conflict.
|
|
177
|
+
def permitted_params(policy)
|
|
178
|
+
params.require(:<%= singular_name %>).permit(*policy.permitted_fields(FIELDS), :lock_version)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
# Generated guardrail tests for <%= class_name %>. They prove the Loam
|
|
4
|
+
# invariants hold for THIS entity: tenant isolation, loud failure without
|
|
5
|
+
# context, audit-by-default, lifecycle events, membership-gated policies.
|
|
6
|
+
# Extend freely; never delete.
|
|
7
|
+
class <%= class_name %>LoamTest < ActiveSupport::TestCase
|
|
8
|
+
setup do
|
|
9
|
+
@tenant_a = Loam::Tenant.create!(name: "Tenant A", slug: "a-<%= file_name %>")
|
|
10
|
+
@tenant_b = Loam::Tenant.create!(name: "Tenant B", slug: "b-<%= file_name %>")
|
|
11
|
+
@manager = User.create!(name: "Manager", email: "manager@example.test", password: "password")
|
|
12
|
+
@employee = User.create!(name: "Employee", email: "employee@example.test", password: "password")
|
|
13
|
+
|
|
14
|
+
with_tenant(@tenant_a) do
|
|
15
|
+
Loam::Membership.create!(user: @manager, role: "manager")
|
|
16
|
+
Loam::Membership.create!(user: @employee, role: "employee")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
test "guardrail: records are invisible outside their tenant" do
|
|
21
|
+
record_id = with_tenant(@tenant_a) { <%= class_name %>.create!(<%= sample_attributes %>).id }
|
|
22
|
+
|
|
23
|
+
with_tenant(@tenant_a) { assert_equal 1, <%= class_name %>.count }
|
|
24
|
+
|
|
25
|
+
with_tenant(@tenant_b) do
|
|
26
|
+
assert_equal 0, <%= class_name %>.count
|
|
27
|
+
assert_raises(ActiveRecord::RecordNotFound) { <%= class_name %>.find(record_id) }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
test "guardrail: touching the model with no tenant context raises" do
|
|
32
|
+
assert_raises(Loam::MissingTenantError) { <%= class_name %>.count }
|
|
33
|
+
assert_raises(Loam::MissingTenantError) { <%= class_name %>.new }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
test "guardrail: a record cannot be written into a foreign tenant" do
|
|
37
|
+
record = with_tenant(@tenant_a) { <%= class_name %>.create!(<%= sample_attributes %>) }
|
|
38
|
+
|
|
39
|
+
with_tenant(@tenant_b) do
|
|
40
|
+
assert_raises(Loam::MissingTenantError) { record.update!(<%= sample_attributes(1) %>) }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
test "every change is audited with actor and changeset" do
|
|
45
|
+
with_tenant(@tenant_a, actor: @manager) do
|
|
46
|
+
record = <%= class_name %>.create!(<%= sample_attributes %>)
|
|
47
|
+
|
|
48
|
+
audit = Loam::AuditRecord.find_by(
|
|
49
|
+
auditable_type: "<%= class_name %>", auditable_id: record.id, action: "create"
|
|
50
|
+
)
|
|
51
|
+
assert audit, "expected an audit record for the create"
|
|
52
|
+
assert_equal @manager.id, audit.actor_id
|
|
53
|
+
assert_includes audit.changeset.keys, "<%= first_field %>"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
test "lifecycle events are published with the tenant stamped" do
|
|
58
|
+
received = []
|
|
59
|
+
subscription = Loam::Events.subscribe("<%= domain %>.<%= singular_name %>.created") do |_name, payload|
|
|
60
|
+
received << payload
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
with_tenant(@tenant_a, actor: @manager) { <%= class_name %>.create!(<%= sample_attributes %>) }
|
|
64
|
+
|
|
65
|
+
assert_equal 1, received.size
|
|
66
|
+
assert_equal @tenant_a.id, received.first[:tenant_id]
|
|
67
|
+
ensure
|
|
68
|
+
ActiveSupport::Notifications.unsubscribe(subscription)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
test "soft-delete hides a record by default, stays tenant-scoped, and restores" do
|
|
72
|
+
record_id = with_tenant(@tenant_a, actor: @manager) do
|
|
73
|
+
record = <%= class_name %>.create!(<%= sample_attributes %>)
|
|
74
|
+
record.soft_delete
|
|
75
|
+
record.id
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
with_tenant(@tenant_a) do
|
|
79
|
+
assert_equal 0, <%= class_name %>.count, "a soft-deleted record is gone from ordinary queries"
|
|
80
|
+
assert_equal 1, <%= class_name %>.with_deleted.count, "but the row still exists"
|
|
81
|
+
assert Loam::AuditRecord.exists?(auditable_type: "<%= class_name %>", auditable_id: record_id, action: "soft_delete")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# with_deleted lifts the deleted_at filter but NOT the tenant filter.
|
|
85
|
+
with_tenant(@tenant_b) do
|
|
86
|
+
assert_equal 0, <%= class_name %>.with_deleted.count, "the recycle bin must never cross tenants"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
with_tenant(@tenant_a) do
|
|
90
|
+
<%= class_name %>.with_deleted.find(record_id).restore
|
|
91
|
+
assert_equal 1, <%= class_name %>.count, "restore returns the record to the default scope"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
test "policy: only members of the current tenant may act" do
|
|
96
|
+
record = with_tenant(@tenant_a) { <%= class_name %>.create!(<%= sample_attributes %>) }
|
|
97
|
+
|
|
98
|
+
with_tenant(@tenant_a) do
|
|
99
|
+
assert <%= class_name %>Policy.new(@manager, record).update?
|
|
100
|
+
|
|
101
|
+
outsider = User.create!(name: "Outsider", email: "outsider@example.test", password: "password")
|
|
102
|
+
outsider_policy = <%= class_name %>Policy.new(outsider, record)
|
|
103
|
+
refute outsider_policy.update?
|
|
104
|
+
refute outsider_policy.writable?(:<%= first_field %>)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
class Create<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::VERSION::STRING.to_f %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :<%= table_name %><%= loam_id_option %> do |t|
|
|
4
|
+
t.references :tenant, null: false, foreign_key: { to_table: :loam_tenants }<%= loam_type_option %>
|
|
5
|
+
<% attributes.each do |attribute| -%>
|
|
6
|
+
<% if encrypted?(attribute) -%>
|
|
7
|
+
t.text :<%= attribute.name %> # encrypted at rest (Loam::Encryptable) — text, since ciphertext outgrows varchar
|
|
8
|
+
<% else -%>
|
|
9
|
+
t.<%= attribute.type %> :<%= attribute.name %>
|
|
10
|
+
<% end -%>
|
|
11
|
+
<% end -%>
|
|
12
|
+
<% encrypt_searchable_names.each do |field| -%>
|
|
13
|
+
t.string :<%= field %>_hash # per-tenant blind index of :<%= field %>, for exact-match lookup
|
|
14
|
+
<% end -%>
|
|
15
|
+
t.json :custom_fields, null: false, default: {}
|
|
16
|
+
t.datetime :deleted_at
|
|
17
|
+
t.integer :lock_version, null: false, default: 0 # optimistic locking (Loam::RecordLock)
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
# Loam::SoftDeletable filters `deleted_at IS NULL` on every query.
|
|
21
|
+
add_index :<%= table_name %>, :deleted_at
|
|
22
|
+
<% encrypt_searchable_names.each do |field| -%>
|
|
23
|
+
add_index :<%= table_name %>, :<%= field %>_hash
|
|
24
|
+
<% end -%>
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Tenant-scoped, audited, evented — by inheritance, not by remembering.
|
|
2
|
+
class <%= class_name %> < Loam::TenantRecord
|
|
3
|
+
include Loam::Auditable
|
|
4
|
+
include Loam::Eventful
|
|
5
|
+
include Loam::CustomFields
|
|
6
|
+
include Loam::Commentable
|
|
7
|
+
include Loam::Attachable
|
|
8
|
+
include Loam::Searchable
|
|
9
|
+
include Loam::SoftDeletable
|
|
10
|
+
<% if encrypted_field_names.any? -%>
|
|
11
|
+
include Loam::Encryptable
|
|
12
|
+
<% end -%>
|
|
13
|
+
|
|
14
|
+
event_domain :<%= domain %>
|
|
15
|
+
<% if searchable_attributes.any? -%>
|
|
16
|
+
searchable_by <%= searchable_attributes.map { |a| ":#{a.name}" }.join(", ") %>
|
|
17
|
+
<% end -%>
|
|
18
|
+
<% options[:encrypt].each do |field| -%>
|
|
19
|
+
encrypts :<%= field %>
|
|
20
|
+
<% end -%>
|
|
21
|
+
<% encrypt_searchable_names.each do |field| -%>
|
|
22
|
+
encrypts :<%= field %>, searchable: true # exact-match lookup via <%= field %>_hash
|
|
23
|
+
<% end -%>
|
|
24
|
+
|
|
25
|
+
# Business logic goes here. Publish business events explicitly:
|
|
26
|
+
# Loam::Events.publish("<%= domain %>.something.happened", id: id)
|
|
27
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class <%= class_name %>Policy < Loam::Policy
|
|
2
|
+
# Action defaults: any member of the current tenant may read/create/update/
|
|
3
|
+
# destroy. Tighten by overriding, e.g.:
|
|
4
|
+
#
|
|
5
|
+
# def destroy? = role == :manager
|
|
6
|
+
#
|
|
7
|
+
# Field-level write access is declared, not coded:
|
|
8
|
+
#
|
|
9
|
+
# field :<%= first_field %>, writable: [:manager]
|
|
10
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<%% policy = Loam::Policy.for(@record) %>
|
|
2
|
+
|
|
3
|
+
<%%# Optimistic-locking conflict: what the user typed vs what is now saved. %>
|
|
4
|
+
<%% if @conflict.present? %>
|
|
5
|
+
<div class="conflict" style="border:2px solid #b00; padding:0.75rem; margin-bottom:1rem">
|
|
6
|
+
<p><strong>This changed while you were editing.</strong> Your entries were not saved. The form now shows the current values — re-apply your changes and save again.</p>
|
|
7
|
+
<table>
|
|
8
|
+
<thead><tr><th>Field</th><th>You entered</th><th>Now saved</th></tr></thead>
|
|
9
|
+
<tbody>
|
|
10
|
+
<%% @conflict.each do |field, values| %>
|
|
11
|
+
<tr><td><%%= field.humanize %></td><td><%%= values["yours"] %></td><td><%%= values["theirs"] %></td></tr>
|
|
12
|
+
<%% end %>
|
|
13
|
+
</tbody>
|
|
14
|
+
</table>
|
|
15
|
+
</div>
|
|
16
|
+
<%% end %>
|
|
17
|
+
|
|
18
|
+
<%%# Advisory lock: warn (do not block) when someone else is editing. %>
|
|
19
|
+
<%% if @lock && @lock.locked_by_id != current_actor.id %>
|
|
20
|
+
<div class="lock-banner" style="background:#ffd; padding:0.5rem; margin-bottom:1rem">
|
|
21
|
+
Locked by <strong><%%= @lock.locked_by.name %></strong> until <%%= @lock.expires_at.strftime("%H:%M") %>.
|
|
22
|
+
You can still save (the version check protects you), but you may collide.
|
|
23
|
+
<%% if current_role == :manager %>
|
|
24
|
+
<%%= button_to "Take over", admin_record_lock_path(lockable_type: @record.class.name, lockable_id: @record.id), method: :delete, form: { style: "display:inline" } %>
|
|
25
|
+
<%% end %>
|
|
26
|
+
</div>
|
|
27
|
+
<%% end %>
|
|
28
|
+
|
|
29
|
+
<%%= form_with model: [:admin, @record] do |f| %>
|
|
30
|
+
<%%= f.hidden_field :lock_version %>
|
|
31
|
+
<%% if @record.errors.any? %>
|
|
32
|
+
<ul>
|
|
33
|
+
<%% @record.errors.full_messages.each do |message| %>
|
|
34
|
+
<li><%%= message %></li>
|
|
35
|
+
<%% end %>
|
|
36
|
+
</ul>
|
|
37
|
+
<%% end %>
|
|
38
|
+
|
|
39
|
+
<% attributes.each do |attribute| -%>
|
|
40
|
+
<%% if policy.writable?(:<%= attribute.name %>) %>
|
|
41
|
+
<p>
|
|
42
|
+
<%%= f.label :<%= attribute.name %> %><br>
|
|
43
|
+
<%%= f.<%= form_field_helper(attribute) %> :<%= attribute.name %> %>
|
|
44
|
+
</p>
|
|
45
|
+
<%% else %>
|
|
46
|
+
<p>
|
|
47
|
+
<%%= f.label :<%= attribute.name %> %>: <%%= @record.<%= attribute.name %> %>
|
|
48
|
+
<em><%%= t("loam.form.read_only") %></em>
|
|
49
|
+
</p>
|
|
50
|
+
<%% end %>
|
|
51
|
+
<% end -%>
|
|
52
|
+
|
|
53
|
+
<%%= render "loam/custom_fields/fields", record: @record, policy: policy, mode: :edit %>
|
|
54
|
+
|
|
55
|
+
<%% if policy.update? %>
|
|
56
|
+
<p>
|
|
57
|
+
<%%= f.label :files, t("loam.form.attach_files") %><br>
|
|
58
|
+
<%%= f.file_field :files, multiple: true %>
|
|
59
|
+
</p>
|
|
60
|
+
<%% end %>
|
|
61
|
+
|
|
62
|
+
<%%= f.submit %>
|
|
63
|
+
<%% end %>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<h1><%%= t("loam.actions.deleted_title", name: <%= class_name %>.model_name.human(count: 2)) %></h1>
|
|
2
|
+
|
|
3
|
+
<p><%%= link_to t("loam.actions.back"), [:admin, <%= class_name %>] %></p>
|
|
4
|
+
|
|
5
|
+
<%%# No Show/Edit here: these rows are hidden by the default scope, so a plain
|
|
6
|
+
find would 404. The one action a deleted record offers is Restore. %>
|
|
7
|
+
<table>
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th><%%= t("loam.labels.id") %></th>
|
|
11
|
+
<% attributes.each do |attribute| -%>
|
|
12
|
+
<th><%%= <%= class_name %>.human_attribute_name("<%= attribute.name %>") %></th>
|
|
13
|
+
<% end -%>
|
|
14
|
+
<th><%%= <%= class_name %>.human_attribute_name("deleted_at") %></th>
|
|
15
|
+
<th></th>
|
|
16
|
+
</tr>
|
|
17
|
+
</thead>
|
|
18
|
+
<tbody>
|
|
19
|
+
<%% @records.each do |record| %>
|
|
20
|
+
<tr>
|
|
21
|
+
<td><%%= record.id %></td>
|
|
22
|
+
<% attributes.each do |attribute| -%>
|
|
23
|
+
<td><%%= record.<%= attribute.name %> %></td>
|
|
24
|
+
<% end -%>
|
|
25
|
+
<td><%%= record.deleted_at %></td>
|
|
26
|
+
<td><%%= button_to t("loam.actions.restore"), [:restore, :admin, record], method: :patch %></td>
|
|
27
|
+
</tr>
|
|
28
|
+
<%% end %>
|
|
29
|
+
</tbody>
|
|
30
|
+
</table>
|
|
31
|
+
|
|
32
|
+
<%%# Page links carry nothing but the page: the recycle bin has no filter box. %>
|
|
33
|
+
<p>
|
|
34
|
+
<%% if @page > 1 %>
|
|
35
|
+
<%%= link_to t("loam.pagination.previous"), polymorphic_path([:deleted, :admin, <%= class_name %>], page: @page - 1) %>
|
|
36
|
+
<%% end %>
|
|
37
|
+
<%%= t("loam.pagination.page", page: @page) %>
|
|
38
|
+
<%% if @has_next %>
|
|
39
|
+
<%%= link_to t("loam.pagination.next"), polymorphic_path([:deleted, :admin, <%= class_name %>], page: @page + 1) %>
|
|
40
|
+
<%% end %>
|
|
41
|
+
</p>
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
<h1><%%= <%= class_name %>.model_name.human(count: 2) %></h1>
|
|
2
|
+
|
|
3
|
+
<%% if @index_partial %>
|
|
4
|
+
<p style="color:#b00"><%%= t("loam.index.incomplete") %></p>
|
|
5
|
+
<%% end %>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<%%= link_to t("loam.actions.new", name: <%= class_name %>.model_name.human), new_polymorphic_path([:admin, <%= class_name %>]) %>
|
|
9
|
+
<%%= link_to t("loam.actions.deleted", name: <%= class_name %>.model_name.human(count: 2)), polymorphic_path([:deleted, :admin, <%= class_name %>]) %>
|
|
10
|
+
<%% if current_role == :manager %>
|
|
11
|
+
<%%= link_to t("loam.actions.export_csv"), url_for(action: :export, **current_index_params) %>
|
|
12
|
+
<%%= link_to t("loam.actions.import_csv"), new_admin_import_path(entity_type: "<%= class_name %>") %>
|
|
13
|
+
<%% end %>
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
<%%# Saved views (Loam::Perspectives): pick one to apply, or save the current one. %>
|
|
17
|
+
<%% if @perspectives.any? %>
|
|
18
|
+
<%%= form_with url: [:admin, <%= class_name %>], method: :get do %>
|
|
19
|
+
<%%= select_tag :perspective_id,
|
|
20
|
+
options_for_select([[t("loam.views.all", name: <%= class_name %>.model_name.human(count: 2)), "none"]] + @perspectives.map { |p| [p.name, p.id] }, @perspective&.id),
|
|
21
|
+
onchange: "this.form.submit()" %>
|
|
22
|
+
<%%= link_to t("loam.views.manage"), admin_perspectives_path(entity_type: "<%= class_name %>") %>
|
|
23
|
+
<%% end %>
|
|
24
|
+
<%% else %>
|
|
25
|
+
<p><%%= link_to t("loam.views.manage_saved"), admin_perspectives_path(entity_type: "<%= class_name %>") %></p>
|
|
26
|
+
<%% end %>
|
|
27
|
+
|
|
28
|
+
<%%= form_with url: admin_perspectives_path, method: :post do %>
|
|
29
|
+
<%%= hidden_field_tag :entity_type, "<%= class_name %>" %>
|
|
30
|
+
<%%= hidden_field_tag :q, params[:q] %>
|
|
31
|
+
<%%= text_field_tag :name, nil, placeholder: t("loam.views.name_placeholder") %>
|
|
32
|
+
<%%= submit_tag t("loam.views.save_current") %>
|
|
33
|
+
<%% end %>
|
|
34
|
+
<% if searchable_attributes.any? -%>
|
|
35
|
+
|
|
36
|
+
<%%= form_with url: [:admin, <%= class_name %>], method: :get do |f| %>
|
|
37
|
+
<%%= text_field_tag :q, params[:q], placeholder: t("loam.views.filter_placeholder", name: <%= class_name %>.model_name.human(count: 2)) %>
|
|
38
|
+
<%%= f.submit t("loam.actions.filter") %>
|
|
39
|
+
<%% end %>
|
|
40
|
+
<% end -%>
|
|
41
|
+
|
|
42
|
+
<%%# Bulk actions on selected rows (Loam::Bulk). Zero selection = no-op; each
|
|
43
|
+
action is policy-checked per record server-side. %>
|
|
44
|
+
<%%= form_with url: url_for(action: :bulk), method: :post do %>
|
|
45
|
+
<p>
|
|
46
|
+
<%%= select_tag :bulk_action, options_for_select([[t("loam.bulk.delete_selected"), "soft_delete"], [t("loam.bulk.set_field"), "set_field"], [t("loam.bulk.export_selected"), "export"]]) %>
|
|
47
|
+
<%%= select_tag :field, options_for_select(%i[<%= field_names.join(" ") %>]) %>
|
|
48
|
+
<%%= text_field_tag :value, nil, placeholder: t("loam.bulk.new_value") %>
|
|
49
|
+
<%%= submit_tag t("loam.bulk.apply"), data: { turbo_confirm: t("loam.bulk.confirm") } %>
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<table>
|
|
53
|
+
<thead>
|
|
54
|
+
<tr>
|
|
55
|
+
<th><input type="checkbox" onclick="document.querySelectorAll('.bulk-select').forEach(function(c){c.checked=this.checked}.bind(this))"></th>
|
|
56
|
+
<th><%%= t("loam.labels.id") %></th>
|
|
57
|
+
<% attributes.each do |attribute| -%>
|
|
58
|
+
<th><%%= sort_link("<%= attribute.name %>", <%= class_name %>.human_attribute_name("<%= attribute.name %>")) %></th>
|
|
59
|
+
<% end -%>
|
|
60
|
+
<th></th>
|
|
61
|
+
</tr>
|
|
62
|
+
</thead>
|
|
63
|
+
<tbody>
|
|
64
|
+
<%% @records.each do |record| %>
|
|
65
|
+
<tr>
|
|
66
|
+
<td><%%= check_box_tag "ids[]", record.id, false, class: "bulk-select" %></td>
|
|
67
|
+
<td><%%= record.id %></td>
|
|
68
|
+
<% attributes.each do |attribute| -%>
|
|
69
|
+
<td><%%= record.<%= attribute.name %> %></td>
|
|
70
|
+
<% end -%>
|
|
71
|
+
<td>
|
|
72
|
+
<%%= link_to t("loam.actions.show"), [:admin, record] %>
|
|
73
|
+
<%%= link_to t("loam.actions.edit"), edit_polymorphic_path([:admin, record]) %>
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
<%% end %>
|
|
77
|
+
</tbody>
|
|
78
|
+
</table>
|
|
79
|
+
<%% end %>
|
|
80
|
+
|
|
81
|
+
<%%# Page links carry the full filter state, or page 2 would quietly drop it. %>
|
|
82
|
+
<p>
|
|
83
|
+
<%% if @page > 1 %>
|
|
84
|
+
<%%= link_to t("loam.pagination.previous"), polymorphic_path([:admin, <%= class_name %>], **current_index_params, page: @page - 1) %>
|
|
85
|
+
<%% end %>
|
|
86
|
+
<%%= t("loam.pagination.page", page: @page) %>
|
|
87
|
+
<%% if @has_next %>
|
|
88
|
+
<%%= link_to t("loam.pagination.next"), polymorphic_path([:admin, <%= class_name %>], **current_index_params, page: @page + 1) %>
|
|
89
|
+
<%% end %>
|
|
90
|
+
</p>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<%% policy = Loam::Policy.for(@record) %>
|
|
2
|
+
|
|
3
|
+
<h1><%%= t("loam.actions.show_title", name: <%= class_name %>.model_name.human, id: @record.id) %></h1>
|
|
4
|
+
|
|
5
|
+
<% attributes.each do |attribute| -%>
|
|
6
|
+
<p><strong><%%= <%= class_name %>.human_attribute_name("<%= attribute.name %>") %>:</strong> <%%= @record.<%= attribute.name %> %></p>
|
|
7
|
+
<% end -%>
|
|
8
|
+
|
|
9
|
+
<%%= render "loam/custom_fields/fields", record: @record, policy: policy, mode: :show %>
|
|
10
|
+
|
|
11
|
+
<%%# Computed cross-module blocks (Loam::Enrichers) — no section when none registered. %>
|
|
12
|
+
<%% enrichments = Loam::Enrichers.enrich(@record) %>
|
|
13
|
+
<%% if enrichments.any? %>
|
|
14
|
+
<h2><%%= t("loam.labels.enrichments") %></h2>
|
|
15
|
+
<dl>
|
|
16
|
+
<%% enrichments.each do |key, value| %>
|
|
17
|
+
<dt><strong><%%= key.humanize %></strong></dt>
|
|
18
|
+
<dd><%%= value %></dd>
|
|
19
|
+
<%% end %>
|
|
20
|
+
</dl>
|
|
21
|
+
<%% end %>
|
|
22
|
+
|
|
23
|
+
<p>
|
|
24
|
+
<%%= link_to t("loam.actions.edit"), edit_polymorphic_path([:admin, @record]) %>
|
|
25
|
+
<%%= link_to "History", admin_history_path(type: @record.class.name, record_id: @record.id) %>
|
|
26
|
+
<%% if @record.class.respond_to?(:loam_translatable_attributes) && @record.class.loam_translatable_attributes.any? && current_role == :manager %>
|
|
27
|
+
<%%= link_to t("loam.actions.translations"), admin_translations_path(translatable_type: @record.class.name, translatable_id: @record.id) %>
|
|
28
|
+
<%% end %>
|
|
29
|
+
<%%# Delete soft-deletes: the record moves to the recycle bin, not oblivion. %>
|
|
30
|
+
<%%= button_to t("loam.actions.destroy"), [:admin, @record], method: :delete if policy.destroy? %>
|
|
31
|
+
<%%= link_to t("loam.actions.back"), [:admin, <%= class_name %>] %>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<%%= render "loam/attachments/attachments", record: @record %>
|
|
35
|
+
|
|
36
|
+
<%%= render "loam/comments/comments", record: @record %>
|