katalyst-koi 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +23 -0
- data/Upgrade.md +6 -0
- data/app/assets/builds/koi/admin.css +1 -0
- data/app/assets/builds/koi/nav_items.css +1 -0
- data/app/assets/config/koi.js +10 -0
- data/app/assets/images/koi/application/chevron-right.svg +10 -0
- data/app/assets/images/koi/application/glyphicons-halflings-white.png +0 -0
- data/app/assets/images/koi/application/glyphicons-halflings.png +0 -0
- data/app/assets/images/koi/application/icon-collapse-down.png +0 -0
- data/app/assets/images/koi/application/icon-collapse-up.png +0 -0
- data/app/assets/images/koi/application/icon-file-doc.png +0 -0
- data/app/assets/images/koi/application/icon-file-img.png +0 -0
- data/app/assets/images/koi/application/icon-file-pdf.png +0 -0
- data/app/assets/images/koi/application/icon-file-ppt.png +0 -0
- data/app/assets/images/koi/application/icon-file-unknown.png +0 -0
- data/app/assets/images/koi/application/icon-file-xls.png +0 -0
- data/app/assets/images/koi/application/icon-file-zip.png +0 -0
- data/app/assets/images/koi/application/icon-form-date-picker.png +0 -0
- data/app/assets/images/koi/application/icon-form-error.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort-ascending.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort-descending.png +0 -0
- data/app/assets/images/koi/application/icon-index-sort.png +0 -0
- data/app/assets/images/koi/application/icon-index-sortable.png +0 -0
- data/app/assets/images/koi/application/icon-menu-cursor.png +0 -0
- data/app/assets/images/koi/application/icon-overlay-add.png +0 -0
- data/app/assets/images/koi/application/icon-overlay-close.png +0 -0
- data/app/assets/images/koi/application/icon-sortable.png +0 -0
- data/app/assets/images/koi/application/jcrop.gif +0 -0
- data/app/assets/images/koi/application/loading.gif +0 -0
- data/app/assets/images/koi/application/select-arrow.svg +3 -0
- data/app/assets/images/koi/application/select_arrow.png +0 -0
- data/app/assets/images/koi/application/sort-ascending.png +0 -0
- data/app/assets/images/koi/application/sort-descending.png +0 -0
- data/app/assets/javascripts/koi/admin.js +4 -0
- data/app/assets/javascripts/koi/controllers/application.js +11 -0
- data/app/assets/javascripts/koi/controllers/document_field_controller.js +26 -0
- data/app/assets/javascripts/koi/controllers/file_field_controller.js +143 -0
- data/app/assets/javascripts/koi/controllers/flash_controller.js +12 -0
- data/app/assets/javascripts/koi/controllers/form_request_submit_controller.js +11 -0
- data/app/assets/javascripts/koi/controllers/image_field_controller.js +24 -0
- data/app/assets/javascripts/koi/controllers/index.js +6 -0
- data/app/assets/javascripts/koi/controllers/index_actions_controller.js +61 -0
- data/app/assets/javascripts/koi/controllers/keyboard_controller.js +149 -0
- data/app/assets/javascripts/koi/controllers/navigation_controller.js +84 -0
- data/app/assets/javascripts/koi/controllers/navigation_toggle_controller.js +7 -0
- data/app/assets/javascripts/koi/controllers/show_hide_controller.js +25 -0
- data/app/assets/javascripts/koi/controllers/sluggable_controller.js +30 -0
- data/app/assets/javascripts/koi/controllers/webauthn_authentication_controller.js +23 -0
- data/app/assets/javascripts/koi/controllers/webauthn_registration_controller.js +30 -0
- data/app/assets/javascripts/koi/utils/transition.js +220 -0
- data/app/assets/stylesheets/koi/admin.scss +27 -0
- data/app/assets/stylesheets/koi/base/_button.scss +122 -0
- data/app/assets/stylesheets/koi/base/_icon.scss +29 -0
- data/app/assets/stylesheets/koi/base/_index.scss +18 -0
- data/app/assets/stylesheets/koi/base/_input.scss +13 -0
- data/app/assets/stylesheets/koi/base/_link.scss +26 -0
- data/app/assets/stylesheets/koi/base/_list.scss +11 -0
- data/app/assets/stylesheets/koi/base/_typography.scss +160 -0
- data/app/assets/stylesheets/koi/components/_actions-group.scss +7 -0
- data/app/assets/stylesheets/koi/components/_image-field.scss +33 -0
- data/app/assets/stylesheets/koi/components/_index-actions.scss +69 -0
- data/app/assets/stylesheets/koi/components/_index-table.scss +91 -0
- data/app/assets/stylesheets/koi/components/_index.scss +6 -0
- data/app/assets/stylesheets/koi/components/_item-table.scss +33 -0
- data/app/assets/stylesheets/koi/components/_pagy.scss +41 -0
- data/app/assets/stylesheets/koi/layouts/_banner.scss +7 -0
- data/app/assets/stylesheets/koi/layouts/_content.scss +40 -0
- data/app/assets/stylesheets/koi/layouts/_flash.scss +41 -0
- data/app/assets/stylesheets/koi/layouts/_header.scss +62 -0
- data/app/assets/stylesheets/koi/layouts/_index.scss +48 -0
- data/app/assets/stylesheets/koi/layouts/_main.scss +23 -0
- data/app/assets/stylesheets/koi/layouts/_navigation.scss +156 -0
- data/app/assets/stylesheets/koi/layouts/_stack.scss +13 -0
- data/app/assets/stylesheets/koi/pages/_index.scss +1 -0
- data/app/assets/stylesheets/koi/pages/_login.scss +40 -0
- data/app/assets/stylesheets/koi/themes/_content.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_govuk.scss +52 -0
- data/app/assets/stylesheets/koi/themes/_index.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_kpop.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_navigation.scss +5 -0
- data/app/assets/stylesheets/koi/themes/_trix.scss +32 -0
- data/app/assets/stylesheets/koi/utils/_breakpoints.scss +13 -0
- data/app/assets/stylesheets/koi/utils/_hide.scss +11 -0
- data/app/assets/stylesheets/koi/utils/_index.scss +2 -0
- data/app/assets/stylesheets/koi/utils/_typography.scss +24 -0
- data/app/components/koi/header/edit_component.rb +58 -0
- data/app/components/koi/header/index_component.rb +23 -0
- data/app/components/koi/header/new_component.rb +40 -0
- data/app/components/koi/header/show_component.rb +51 -0
- data/app/components/koi/header_component.html.erb +16 -0
- data/app/components/koi/header_component.rb +28 -0
- data/app/components/koi/index_table_component.rb +21 -0
- data/app/controllers/admin/admin_users_controller.rb +88 -0
- data/app/controllers/admin/application_controller.rb +9 -0
- data/app/controllers/admin/caches_controller.rb +11 -0
- data/app/controllers/admin/credentials_controller.rb +64 -0
- data/app/controllers/admin/dashboards_controller.rb +7 -0
- data/app/controllers/admin/sessions_controller.rb +78 -0
- data/app/controllers/admin/url_rewrites_controller.rb +87 -0
- data/app/controllers/concerns/koi/controller/has_admin_users.rb +49 -0
- data/app/controllers/concerns/koi/controller/has_webauthn.rb +45 -0
- data/app/controllers/concerns/koi/controller/is_admin_controller.rb +52 -0
- data/app/helpers/katalyst/content/editor/errors.rb +21 -0
- data/app/helpers/katalyst/navigation/editor/errors.rb +21 -0
- data/app/helpers/koi/application_helper.rb +7 -0
- data/app/helpers/koi/date_helper.rb +36 -0
- data/app/helpers/koi/definition_list_helper.rb +92 -0
- data/app/helpers/koi/index_actions_helper.rb +99 -0
- data/app/jobs/koi/application_job.rb +6 -0
- data/app/mailers/koi/application_mailer.rb +8 -0
- data/app/models/admin/credential.rb +14 -0
- data/app/models/admin/user.rb +51 -0
- data/app/models/application_record.rb +5 -0
- data/app/models/concerns/koi/model/archivable.rb +55 -0
- data/app/models/url_rewrite.rb +25 -0
- data/app/views/admin/admin_users/_admin.html+row.erb +4 -0
- data/app/views/admin/admin_users/_authentication.html.erb +15 -0
- data/app/views/admin/admin_users/_fields.html.erb +4 -0
- data/app/views/admin/admin_users/edit.html.erb +11 -0
- data/app/views/admin/admin_users/index.html.erb +9 -0
- data/app/views/admin/admin_users/new.html.erb +11 -0
- data/app/views/admin/admin_users/show.html.erb +22 -0
- data/app/views/admin/credentials/new.html.erb +14 -0
- data/app/views/admin/dashboards/show.html.erb +1 -0
- data/app/views/admin/sessions/new.html.erb +19 -0
- data/app/views/admin/shared/icons/_close.html.erb +8 -0
- data/app/views/admin/shared/icons/_cross.html.erb +3 -0
- data/app/views/admin/shared/icons/_menu.html.erb +3 -0
- data/app/views/admin/shared/icons/_refresh.html.erb +8 -0
- data/app/views/admin/url_rewrites/_form_fields.html.erb +3 -0
- data/app/views/admin/url_rewrites/_url_rewrite.html+row.erb +7 -0
- data/app/views/admin/url_rewrites/edit.html.erb +12 -0
- data/app/views/admin/url_rewrites/index.html.erb +10 -0
- data/app/views/admin/url_rewrites/new.html.erb +11 -0
- data/app/views/admin/url_rewrites/show.html.erb +16 -0
- data/app/views/katalyst/content/asides/_aside.html+form.erb +18 -0
- data/app/views/katalyst/content/columns/_column.html+form.erb +18 -0
- data/app/views/katalyst/content/contents/_content.html+form.erb +20 -0
- data/app/views/katalyst/content/figures/_figure.html+form.erb +17 -0
- data/app/views/katalyst/content/groups/_group.html+form.erb +18 -0
- data/app/views/katalyst/content/items/_item.html+form.erb +18 -0
- data/app/views/katalyst/content/sections/_section.html+form.erb +18 -0
- data/app/views/katalyst/navigation/items/_button.html.erb +15 -0
- data/app/views/katalyst/navigation/items/_heading.html.erb +11 -0
- data/app/views/katalyst/navigation/items/_link.html.erb +13 -0
- data/app/views/katalyst/navigation/menus/edit.html.erb +12 -0
- data/app/views/katalyst/navigation/menus/new.html.erb +9 -0
- data/app/views/katalyst/navigation/menus/show.html.erb +18 -0
- data/app/views/layouts/koi/_environment.html.erb +4 -0
- data/app/views/layouts/koi/_flash.html.erb +8 -0
- data/app/views/layouts/koi/_header.html.erb +11 -0
- data/app/views/layouts/koi/_navigation.html.erb +13 -0
- data/app/views/layouts/koi/_navigation_collapse.html.erb +3 -0
- data/app/views/layouts/koi/_navigation_header.html.erb +6 -0
- data/app/views/layouts/koi/_navigation_item.html.erb +12 -0
- data/app/views/layouts/koi/application.html.erb +59 -0
- data/app/views/layouts/koi/login.html.erb +29 -0
- data/config/importmap.rb +9 -0
- data/config/initializers/flipper.rb +13 -0
- data/config/initializers/pagy.rb +1 -0
- data/config/initializers/time_formats.rb +5 -0
- data/config/locales/koi.en.yml +18 -0
- data/config/locales/pagy.en.yml +6 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20120220130849_devise_create_admins.rb +56 -0
- data/db/migrate/20130509235316_add_url_rewriter.rb +13 -0
- data/db/migrate/20230213053854_convert_devise_admins_to_rails.rb +7 -0
- data/db/migrate/20230412023411_create_admin_user_credentials.rb +20 -0
- data/db/migrate/20230531063707_update_admin_users.rb +37 -0
- data/db/migrate/20230602033610_add_archived_to_admin_users.rb +7 -0
- data/db/seeds.rb +9 -0
- data/lib/generators/koi/active_record/active_record_generator.rb +43 -0
- data/lib/generators/koi/admin/USAGE +8 -0
- data/lib/generators/koi/admin/admin_generator.rb +20 -0
- data/lib/generators/koi/admin_controller/USAGE +17 -0
- data/lib/generators/koi/admin_controller/admin_controller_generator.rb +51 -0
- data/lib/generators/koi/admin_controller/templates/controller.rb.tt +81 -0
- data/lib/generators/koi/admin_controller/templates/controller_spec.rb.tt +135 -0
- data/lib/generators/koi/admin_route/admin_route_generator.rb +62 -0
- data/lib/generators/koi/admin_views/USAGE +12 -0
- data/lib/generators/koi/admin_views/admin_views_generator.rb +54 -0
- data/lib/generators/koi/admin_views/templates/_fields.html.erb.tt +3 -0
- data/lib/generators/koi/admin_views/templates/_record.html+row.erb.tt +10 -0
- data/lib/generators/koi/admin_views/templates/edit.html.erb.tt +12 -0
- data/lib/generators/koi/admin_views/templates/index.html.erb.tt +7 -0
- data/lib/generators/koi/admin_views/templates/new.html.erb.tt +11 -0
- data/lib/generators/koi/admin_views/templates/show.html.erb.tt +18 -0
- data/lib/govuk_design_system_formbuilder/concerns/file_element.rb +115 -0
- data/lib/govuk_design_system_formbuilder/elements/document.rb +59 -0
- data/lib/govuk_design_system_formbuilder/elements/image.rb +86 -0
- data/lib/katalyst/koi.rb +3 -0
- data/lib/koi/caching.rb +15 -0
- data/lib/koi/config.rb +11 -0
- data/lib/koi/engine.rb +40 -0
- data/lib/koi/form_builder.rb +76 -0
- data/lib/koi/menu/builder.rb +68 -0
- data/lib/koi/menu.rb +46 -0
- data/lib/koi/middleware/url_redirect.rb +44 -0
- data/lib/koi/release.rb +52 -0
- data/lib/koi/version.rb +5 -0
- data/lib/koi.rb +37 -0
- data/spec/factories/admins.rb +9 -0
- data/spec/factories/url_rewrites.rb +9 -0
- metadata +430 -0
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class UrlRewritesController < ApplicationController
|
5
|
+
before_action :set_url_rewrite, only: %i[show edit update destroy]
|
6
|
+
|
7
|
+
def index
|
8
|
+
collection = Collection.new.with_params(params).apply(UrlRewrite.strict_loading)
|
9
|
+
table = Koi::IndexTableComponent.new(collection:)
|
10
|
+
|
11
|
+
respond_to do |format|
|
12
|
+
format.turbo_stream { render(table) } if self_referred?
|
13
|
+
format.html { render :index, locals: { table:, collection: } }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def show
|
18
|
+
render :show, locals: { url_rewrite: @url_rewrite }
|
19
|
+
end
|
20
|
+
|
21
|
+
def new
|
22
|
+
@url_rewrite = UrlRewrite.new
|
23
|
+
render :new, locals: { url_rewrite: @url_rewrite }
|
24
|
+
end
|
25
|
+
|
26
|
+
def edit
|
27
|
+
render :edit, locals: { url_rewrite: @url_rewrite }
|
28
|
+
end
|
29
|
+
|
30
|
+
def create
|
31
|
+
@url_rewrite = UrlRewrite.new(url_rewrite_params)
|
32
|
+
|
33
|
+
if @url_rewrite.save
|
34
|
+
redirect_to admin_url_rewrite_path(@url_rewrite)
|
35
|
+
else
|
36
|
+
render :new, status: :unprocessable_entity, locals: { url_rewrite: @url_rewrite }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def update
|
41
|
+
@url_rewrite.attributes = url_rewrite_params
|
42
|
+
|
43
|
+
if @url_rewrite.save
|
44
|
+
redirect_to admin_url_rewrite_path(@url_rewrite)
|
45
|
+
else
|
46
|
+
render :edit, status: :unprocessable_entity, locals: { url_rewrite: @url_rewrite }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def destroy
|
51
|
+
@url_rewrite.destroy!
|
52
|
+
|
53
|
+
redirect_to admin_url_rewrites_path
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def url_rewrite_params
|
59
|
+
params.require(:url_rewrite).permit(:from, :to, :active)
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_url_rewrite
|
63
|
+
@url_rewrite = UrlRewrite.find(params[:id])
|
64
|
+
end
|
65
|
+
|
66
|
+
class Collection < Katalyst::Tables::Collection::Base
|
67
|
+
attribute :search, :string, default: ""
|
68
|
+
attribute :scope, :string, default: "active"
|
69
|
+
|
70
|
+
config.sorting = "from"
|
71
|
+
config.paginate = true
|
72
|
+
|
73
|
+
def filter
|
74
|
+
self.items = items.admin_search(search) if search.present?
|
75
|
+
|
76
|
+
self.items = case scope&.to_sym
|
77
|
+
when :active
|
78
|
+
items.where(active: true)
|
79
|
+
when :inactive
|
80
|
+
items.where(active: false)
|
81
|
+
else
|
82
|
+
items
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Controller
|
5
|
+
module HasAdminUsers
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
helper_method :admin_signed_in?
|
10
|
+
helper_method :current_admin_user
|
11
|
+
helper_method :current_admin
|
12
|
+
end
|
13
|
+
|
14
|
+
def admin_signed_in?
|
15
|
+
current_admin_user.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_admin_user
|
19
|
+
@current_admin_user ||= Admin::User.find(session[:admin_user_id]) if session[:admin_user_id].present?
|
20
|
+
end
|
21
|
+
|
22
|
+
# @deprecated Use current_admin_user instead
|
23
|
+
alias_method :current_admin, :current_admin_user
|
24
|
+
|
25
|
+
module Test
|
26
|
+
# Include in view specs to stub out the current admin user
|
27
|
+
module ViewHelper
|
28
|
+
extend ActiveSupport::Concern
|
29
|
+
|
30
|
+
included do
|
31
|
+
before do
|
32
|
+
view.singleton_class.module_eval do
|
33
|
+
def admin_signed_in?
|
34
|
+
current_admin_user.present?
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_admin_user
|
38
|
+
respond_to?(:admin_user) ? admin_user : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
alias_method :current_admin, :current_admin_user
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Controller
|
5
|
+
module HasWebauthn
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
helper_method :webauthn_auth_options
|
10
|
+
end
|
11
|
+
|
12
|
+
def webauthn_relying_party
|
13
|
+
@webauthn_relying_party ||=
|
14
|
+
WebAuthn::RelyingParty.new(
|
15
|
+
name: "Koi Admin",
|
16
|
+
origin: request.base_url,
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def webauthn_auth_options
|
21
|
+
options = webauthn_relying_party.options_for_authentication(
|
22
|
+
allow: Admin::Credential.pluck(:external_id),
|
23
|
+
)
|
24
|
+
session[:authentication_challenge] = options.challenge
|
25
|
+
|
26
|
+
options
|
27
|
+
end
|
28
|
+
|
29
|
+
def webauthn_authenticate!
|
30
|
+
return if session_params[:response].blank?
|
31
|
+
|
32
|
+
webauthn_credential, stored_credential = webauthn_relying_party.verify_authentication(
|
33
|
+
JSON.parse(session_params[:response]),
|
34
|
+
session[:authentication_challenge],
|
35
|
+
) do |credential|
|
36
|
+
Admin::Credential.find_by!(external_id: credential.id)
|
37
|
+
end
|
38
|
+
|
39
|
+
stored_credential.update!(sign_count: webauthn_credential.sign_count)
|
40
|
+
|
41
|
+
stored_credential.admin
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Controller
|
5
|
+
module IsAdminController
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
class_methods do
|
9
|
+
def authenticate_local_admins(value)
|
10
|
+
Koi::Controller::IsAdminController.authenticate_local_admins = value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
included do
|
15
|
+
include HasAdminUsers
|
16
|
+
include Katalyst::Tables::Backend
|
17
|
+
include Pagy::Backend
|
18
|
+
|
19
|
+
default_form_builder "Koi::FormBuilder"
|
20
|
+
|
21
|
+
helper Katalyst::GOVUK::Formbuilder::Frontend
|
22
|
+
helper Katalyst::Navigation::FrontendHelper
|
23
|
+
helper Katalyst::Tables::Frontend
|
24
|
+
helper Pagy::Frontend
|
25
|
+
helper IndexActionsHelper
|
26
|
+
helper :all
|
27
|
+
|
28
|
+
layout "koi/application"
|
29
|
+
|
30
|
+
before_action :authenticate_local_admin, if: -> { Koi::Controller::IsAdminController.authenticate_local_admins }
|
31
|
+
before_action :authenticate_admin, unless: :admin_signed_in?
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
attr_accessor :authenticate_local_admins
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def authenticate_local_admin
|
41
|
+
return if admin_signed_in? || !Rails.env.development?
|
42
|
+
|
43
|
+
session[:admin_user_id] =
|
44
|
+
Admin::User.where(email: %W[#{ENV['USER']}@katalyst.com.au admin@katalyst.com.au]).first&.id
|
45
|
+
end
|
46
|
+
|
47
|
+
def authenticate_admin
|
48
|
+
redirect_to new_admin_session_path, status: :temporary_redirect
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Content
|
5
|
+
module Editor
|
6
|
+
class Errors < Base
|
7
|
+
def build(**options)
|
8
|
+
turbo_frame_tag dom_id(container, :errors) do
|
9
|
+
form_builder.govuk_error_summary(**options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def form_builder
|
16
|
+
Koi::FormBuilder.new(container.model_name.param_key, container, self, {})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Katalyst
|
4
|
+
module Navigation
|
5
|
+
module Editor
|
6
|
+
class Errors < Base
|
7
|
+
def build(**options)
|
8
|
+
turbo_frame_tag dom_id(menu, :errors) do
|
9
|
+
form_builder.govuk_error_summary(**options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def form_builder
|
16
|
+
Koi::FormBuilder.new(menu.model_name.param_key, menu, self, {})
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Naming/MethodName
|
4
|
+
module Koi
|
5
|
+
module DateHelper
|
6
|
+
# @deprecated
|
7
|
+
def date_format(date, format)
|
8
|
+
date.strftime format.gsub(/yyyy/, "%Y")
|
9
|
+
.gsub(/yy/, "%y")
|
10
|
+
.gsub(/Month/, "%B")
|
11
|
+
.gsub(/M/, "%b")
|
12
|
+
.gsub(/mm/, "%m")
|
13
|
+
.gsub(/m/, "%-m")
|
14
|
+
.gsub(/Day/, "%A")
|
15
|
+
.gsub(/D/, "%a")
|
16
|
+
.gsub(/dd/, "%d")
|
17
|
+
.gsub(/d/, "%-d")
|
18
|
+
end
|
19
|
+
|
20
|
+
# @deprecated
|
21
|
+
def date_Month_d_yyyy(date)
|
22
|
+
date.strftime "%B %-d, %Y"
|
23
|
+
end
|
24
|
+
|
25
|
+
# @deprecated
|
26
|
+
def date_d_Month_yyyy(date)
|
27
|
+
date.strftime "%-d %B %Y"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @deprecated
|
31
|
+
def date_d_M_yy(date)
|
32
|
+
date.strftime "%-d %b %y"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
# rubocop:enable Naming/MethodName
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module DefinitionListHelper
|
5
|
+
def definition_list(**options, &)
|
6
|
+
DefinitionListBuilder.new(self, options).render(&)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class DefinitionListBuilder
|
11
|
+
delegate_missing_to :@context
|
12
|
+
|
13
|
+
def initialize(context, options = {})
|
14
|
+
@context = context
|
15
|
+
@options = options
|
16
|
+
end
|
17
|
+
|
18
|
+
def render(&block)
|
19
|
+
tag.dl class: @options.delete(:class) do
|
20
|
+
concat(capture { yield self }) if block
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def items_with(model:, attributes:, **options)
|
25
|
+
capture do
|
26
|
+
attributes.each do |attribute|
|
27
|
+
concat item(model, attribute, **options)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def item(object, attribute, **options, &)
|
33
|
+
Definition.new(@context, object, attribute, **@options, **options).render(&)
|
34
|
+
end
|
35
|
+
|
36
|
+
class Definition
|
37
|
+
attr_reader :object, :attribute, :options
|
38
|
+
|
39
|
+
delegate_missing_to :@context
|
40
|
+
|
41
|
+
def initialize(context, object, attribute, options = {})
|
42
|
+
@context = context
|
43
|
+
@object = object
|
44
|
+
@attribute = attribute
|
45
|
+
@options = options
|
46
|
+
end
|
47
|
+
|
48
|
+
def render(&)
|
49
|
+
return unless render?
|
50
|
+
|
51
|
+
term_tag + definition_tag(&)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def render?
|
57
|
+
!(options.fetch(:skip_blank, true) && attribute_value.blank? && attribute_value != false)
|
58
|
+
end
|
59
|
+
|
60
|
+
def term_tag
|
61
|
+
tag.dt(label)
|
62
|
+
end
|
63
|
+
|
64
|
+
def definition_tag(&block)
|
65
|
+
if block
|
66
|
+
tag.dd { yield attribute_value }
|
67
|
+
else
|
68
|
+
case attribute_value
|
69
|
+
when Array
|
70
|
+
tag.dd(attribute_value.join(", "))
|
71
|
+
when ActiveStorage::Attached::One
|
72
|
+
tag.dd(attribute_value.attached? ? link_to(attribute_value.filename, url_for(attribute_value)) : "")
|
73
|
+
when Date, Time, DateTime
|
74
|
+
tag.dd(l(attribute_value, format: :display))
|
75
|
+
when TrueClass, FalseClass
|
76
|
+
tag.dd(attribute_value ? "Yes" : "No")
|
77
|
+
else
|
78
|
+
tag.dd(attribute_value.to_s)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def label
|
84
|
+
options.dig(:label, :text) || object.class.human_attribute_name(attribute)
|
85
|
+
end
|
86
|
+
|
87
|
+
def attribute_value
|
88
|
+
@attribute_value ||= object.public_send(attribute)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module IndexActionsHelper
|
5
|
+
def koi_index_actions(search: false, create: false, &block)
|
6
|
+
IndexActionsBuilder.new(self, search:, create:).render(&block)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class IndexActionsBuilder
|
11
|
+
delegate_missing_to :@context
|
12
|
+
|
13
|
+
def initialize(context, search:, create:)
|
14
|
+
@context = context
|
15
|
+
@search = search
|
16
|
+
@create = create
|
17
|
+
end
|
18
|
+
|
19
|
+
def search?
|
20
|
+
!!@search
|
21
|
+
end
|
22
|
+
|
23
|
+
def create?
|
24
|
+
!!@create
|
25
|
+
end
|
26
|
+
|
27
|
+
def render(&)
|
28
|
+
tag.nav class: "index-actions", data: { controller: "index-actions", action: actions } do
|
29
|
+
form_with(**search_options,
|
30
|
+
data: { controller: "search",
|
31
|
+
turbo_stream: "",
|
32
|
+
action: <<~ACTIONS,
|
33
|
+
input->index-actions#update
|
34
|
+
change->index-actions#update
|
35
|
+
submit->index-actions#submit
|
36
|
+
ACTIONS
|
37
|
+
}) do |form|
|
38
|
+
concat(links(form, &))
|
39
|
+
concat(sort_input(form))
|
40
|
+
concat(search(form)) if create? || search?
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def search(form)
|
46
|
+
tag.div class: "actions-group" do
|
47
|
+
concat(search_button(form)) if search?
|
48
|
+
concat(create_button(form)) if create?
|
49
|
+
concat(search_input(form)) if search?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def sort_input(form)
|
54
|
+
form.hidden_field(:sort, value: params[:sort], data: { index_actions_target: "sort" })
|
55
|
+
end
|
56
|
+
|
57
|
+
# Hidden button to trigger search, avoids triggering create instead.
|
58
|
+
def search_button(form)
|
59
|
+
form.submit("Search", hidden: "")
|
60
|
+
end
|
61
|
+
|
62
|
+
def create_button(_form)
|
63
|
+
tag.button(t("koi.labels.new", default: "New"),
|
64
|
+
type: :submit,
|
65
|
+
formaction: @create == true ? url_for(action: :new) : new_polymorphic_path(@create),
|
66
|
+
class: "button--primary",
|
67
|
+
data: { index_actions_target: "create" })
|
68
|
+
end
|
69
|
+
|
70
|
+
def search_input(form)
|
71
|
+
form.search_field(:search,
|
72
|
+
placeholder: t("koi.labels.search", default: "Search"),
|
73
|
+
value: params[:search],
|
74
|
+
data: { index_actions_target: "search" })
|
75
|
+
end
|
76
|
+
|
77
|
+
def links(form, &)
|
78
|
+
tag.div(class: "actions-group") do
|
79
|
+
yield(form) if block_given?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def actions
|
84
|
+
[
|
85
|
+
("shortcut:cancel@document->index-actions#clear" if search?),
|
86
|
+
("shortcut:create@document->index-actions#create" if create?),
|
87
|
+
("shortcut:search@document->index-actions#search" if search?),
|
88
|
+
"shortcut:page-prev@document->index-actions#prevPage",
|
89
|
+
"shortcut:page-next@document->index-actions#nextPage",
|
90
|
+
].compact.join(" ")
|
91
|
+
end
|
92
|
+
|
93
|
+
def search_options
|
94
|
+
options = { url: request.path, method: :get, scope: "" }
|
95
|
+
options.merge!(@search) if @search.is_a?(Hash)
|
96
|
+
options
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class Credential < ApplicationRecord
|
5
|
+
self.table_name = :admin_credentials
|
6
|
+
|
7
|
+
belongs_to :admin, class_name: "Admin::User", inverse_of: :credentials
|
8
|
+
|
9
|
+
validates :external_id, :public_key, :nickname, :sign_count, presence: true
|
10
|
+
validates :external_id, uniqueness: true
|
11
|
+
validates :sign_count,
|
12
|
+
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Admin
|
4
|
+
class User < ApplicationRecord
|
5
|
+
include Koi::Model::Archivable
|
6
|
+
|
7
|
+
def self.model_name
|
8
|
+
ActiveModel::Name.new(self, nil, "Admin")
|
9
|
+
end
|
10
|
+
|
11
|
+
has_secure_password :password
|
12
|
+
|
13
|
+
has_many :credentials, inverse_of: :admin, class_name: "Admin::Credential", dependent: :destroy
|
14
|
+
|
15
|
+
validates :name, :email, presence: true
|
16
|
+
validates :email, uniqueness: true, format: { with: URI::MailTo::EMAIL_REGEXP }
|
17
|
+
|
18
|
+
scope :alphabetical, -> { order(name: :asc) }
|
19
|
+
|
20
|
+
if "PgSearch::Model".safe_constantize
|
21
|
+
include PgSearch::Model
|
22
|
+
|
23
|
+
pg_search_scope :admin_search, against: %i[email name], using: { tsearch: { prefix: true } }
|
24
|
+
else
|
25
|
+
scope :admin_search, ->(query) do
|
26
|
+
where("email LIKE :query OR name LIKE :query", query: "%#{query}%")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# TODO(sfn) remove once Rails 7.1 is released
|
31
|
+
# https://edgeapi.rubyonrails.org/classes/ActiveRecord/SecurePassword/ClassMethods.html#method-i-authenticate_by
|
32
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
33
|
+
def self.authenticate_by(attributes)
|
34
|
+
passwords, identifiers = attributes.to_h.partition do |name, _value|
|
35
|
+
!has_attribute?(name) && has_attribute?("#{name}_digest")
|
36
|
+
end.map(&:to_h)
|
37
|
+
|
38
|
+
raise ArgumentError, "One or more password arguments are required" if passwords.empty?
|
39
|
+
raise ArgumentError, "One or more finder arguments are required" if identifiers.empty?
|
40
|
+
|
41
|
+
if (record = find_by(identifiers))
|
42
|
+
record if passwords.count { |name, value| record.public_send(:"authenticate_#{name}", value) } == passwords.size
|
43
|
+
else
|
44
|
+
new(passwords)
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Koi
|
4
|
+
module Model
|
5
|
+
module Archivable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
scope :not_archived, -> { where(archived_at: nil) }
|
10
|
+
scope :archived, -> { unscope(where: :archived_at).where.not(archived_at: nil) }
|
11
|
+
scope :with_archived, -> { unscope(where: :archived_at) }
|
12
|
+
|
13
|
+
default_scope { not_archived }
|
14
|
+
|
15
|
+
alias_method :archived?, :archived
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns true iff the record has been archived.
|
19
|
+
def archived
|
20
|
+
archived_at.present?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Update archived status based on given boolean value.
|
24
|
+
def archived=(archived)
|
25
|
+
if ActiveRecord::Type::Boolean.new.cast(archived)
|
26
|
+
archive
|
27
|
+
else
|
28
|
+
restore
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Mark a record as archived. It will no longer appear in default queries.
|
33
|
+
def archive
|
34
|
+
self.archived_at = Time.current
|
35
|
+
end
|
36
|
+
|
37
|
+
# Archive a record immediately, without validation.
|
38
|
+
def archive!
|
39
|
+
archive
|
40
|
+
save!(validate: false)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Mark a record as no longer archived. It will appear in default queries.
|
44
|
+
def restore
|
45
|
+
self.archived_at = nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Restore a record immediately, without validation.
|
49
|
+
def restore!
|
50
|
+
restore
|
51
|
+
save!(validate: false)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class UrlRewrite < ApplicationRecord
|
4
|
+
validates :from, :to, presence: true
|
5
|
+
validates :from, format: { with: %r{\A/.*\z}, message: "should start with /" }
|
6
|
+
|
7
|
+
scope :active, -> { where(active: true) }
|
8
|
+
scope :alphabetical, -> { order(from: :asc) }
|
9
|
+
|
10
|
+
def to_s
|
11
|
+
"#{from} -> #{to}"
|
12
|
+
end
|
13
|
+
|
14
|
+
scope :admin_search, ->(query) do
|
15
|
+
where(arel_table[:from].matches("%#{query}%").or(arel_table[:to].matches("%#{query}%")))
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.redirect_path_for(path)
|
19
|
+
active.find_by(from: path).try(:to)
|
20
|
+
end
|
21
|
+
|
22
|
+
def title
|
23
|
+
from.delete_prefix("/")
|
24
|
+
end
|
25
|
+
end
|