panda-core 0.12.5 → 1.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 +4 -4
- data/LICENSE +1 -1
- data/README.md +53 -0
- data/app/assets/builds/panda_core_admin.css +2 -0
- data/app/assets/tailwind/application.css +236 -36
- data/app/builders/panda/core/form_builder.rb +120 -18
- data/app/components/panda/core/UI/badge.html.erb +10 -0
- data/app/components/panda/core/UI/badge.rb +14 -30
- data/app/components/panda/core/UI/button.html.erb +5 -0
- data/app/components/panda/core/UI/button.rb +11 -15
- data/app/components/panda/core/UI/card.html.erb +3 -0
- data/app/components/panda/core/UI/card.rb +7 -6
- data/app/components/panda/core/admin/attribute_diff_component.html.erb +21 -0
- data/app/components/panda/core/admin/attribute_diff_component.rb +29 -0
- data/app/components/panda/core/admin/breadcrumb_component.html.erb +36 -0
- data/app/components/panda/core/admin/breadcrumb_component.rb +12 -64
- data/app/components/panda/core/admin/breadcrumbs_component.html.erb +16 -0
- data/app/components/panda/core/admin/breadcrumbs_component.rb +26 -0
- data/app/components/panda/core/admin/button_component.html.erb +9 -0
- data/app/components/panda/core/admin/button_component.rb +36 -31
- data/app/components/panda/core/admin/callout_component.html.erb +17 -0
- data/app/components/panda/core/admin/callout_component.rb +57 -0
- data/app/components/panda/core/admin/code_block_component.html.erb +9 -0
- data/app/components/panda/core/admin/code_block_component.rb +23 -0
- data/app/components/panda/core/admin/container_component.html.erb +27 -0
- data/app/components/panda/core/admin/container_component.rb +11 -77
- data/app/components/panda/core/admin/contributors_summary_component.html.erb +15 -0
- data/app/components/panda/core/admin/contributors_summary_component.rb +23 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.html.erb +70 -0
- data/app/components/panda/core/admin/dashboard_default_content_component.rb +32 -0
- data/app/components/panda/core/admin/delete_button_component.html.erb +4 -0
- data/app/components/panda/core/admin/delete_button_component.rb +41 -0
- data/app/components/panda/core/admin/dropdown_button_component.html.erb +28 -0
- data/app/components/panda/core/admin/dropdown_button_component.rb +100 -0
- data/app/components/panda/core/admin/dropdown_component.html.erb +32 -0
- data/app/components/panda/core/admin/dropdown_component.rb +65 -0
- data/app/components/panda/core/admin/empty_state_component.html.erb +16 -0
- data/app/components/panda/core/admin/empty_state_component.rb +25 -0
- data/app/components/panda/core/admin/file_gallery_component.html.erb +58 -0
- data/app/components/panda/core/admin/file_gallery_component.rb +26 -98
- data/app/components/panda/core/admin/filter_pills_component.html.erb +6 -0
- data/app/components/panda/core/admin/filter_pills_component.rb +29 -0
- data/app/components/panda/core/admin/flash_message_component.html.erb +11 -0
- data/app/components/panda/core/admin/flash_message_component.rb +33 -60
- data/app/components/panda/core/admin/form_error_component.html.erb +7 -0
- data/app/components/panda/core/admin/form_error_component.rb +8 -16
- data/app/components/panda/core/admin/form_footer_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_footer_component.rb +65 -0
- data/app/components/panda/core/admin/form_input_component.html.erb +1 -0
- data/app/components/panda/core/admin/form_input_component.rb +21 -13
- data/app/components/panda/core/admin/form_section_component.html.erb +16 -0
- data/app/components/panda/core/admin/form_section_component.rb +64 -0
- data/app/components/panda/core/admin/form_select_component.html.erb +5 -0
- data/app/components/panda/core/admin/form_select_component.rb +25 -26
- data/app/components/panda/core/admin/heading_component.html.erb +26 -0
- data/app/components/panda/core/admin/heading_component.rb +23 -60
- data/app/components/panda/core/admin/login_form_component.html.erb +38 -0
- data/app/components/panda/core/admin/login_form_component.rb +28 -0
- data/app/components/panda/core/admin/main_layout_component.html.erb +88 -0
- data/app/components/panda/core/admin/main_layout_component.rb +19 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.html.erb +41 -0
- data/app/components/panda/core/admin/my_profile/connected_account_component.rb +27 -0
- data/app/components/panda/core/admin/navigation/item_component.html.erb +34 -0
- data/app/components/panda/core/admin/navigation/item_component.rb +69 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.html.erb +9 -0
- data/app/components/panda/core/admin/navigation/sub_item_component.rb +44 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.html.erb +30 -0
- data/app/components/panda/core/admin/navigation/user_menu_component.rb +36 -0
- data/app/components/panda/core/admin/page_header_component.html.erb +28 -0
- data/app/components/panda/core/admin/page_header_component.rb +19 -35
- data/app/components/panda/core/admin/pagination_component.html.erb +33 -0
- data/app/components/panda/core/admin/pagination_component.rb +56 -0
- data/app/components/panda/core/admin/panel_component.html.erb +19 -0
- data/app/components/panda/core/admin/panel_component.rb +4 -29
- data/app/components/panda/core/admin/prose_panel_component.html.erb +6 -0
- data/app/components/panda/core/admin/prose_panel_component.rb +43 -0
- data/app/components/panda/core/admin/search_bar_component.html.erb +19 -0
- data/app/components/panda/core/admin/search_bar_component.rb +31 -0
- data/app/components/panda/core/admin/search_filter_bar_component.html.erb +22 -0
- data/app/components/panda/core/admin/search_filter_bar_component.rb +27 -0
- data/app/components/panda/core/admin/secret_field_component.html.erb +24 -0
- data/app/components/panda/core/admin/secret_field_component.rb +31 -0
- data/app/components/panda/core/admin/sidebar_component.html.erb +160 -0
- data/app/components/panda/core/admin/sidebar_component.rb +55 -0
- data/app/components/panda/core/admin/slideover_component.html.erb +67 -0
- data/app/components/panda/core/admin/slideover_component.rb +31 -70
- data/app/components/panda/core/admin/statistics_component.html.erb +4 -0
- data/app/components/panda/core/admin/statistics_component.rb +6 -8
- data/app/components/panda/core/admin/tab_bar_component.html.erb +48 -0
- data/app/components/panda/core/admin/tab_bar_component.rb +32 -67
- data/app/components/panda/core/admin/table_component.html.erb +82 -0
- data/app/components/panda/core/admin/table_component.rb +54 -87
- data/app/components/panda/core/admin/tag_badge_component.html.erb +7 -0
- data/app/components/panda/core/admin/tag_badge_component.rb +21 -0
- data/app/components/panda/core/admin/tag_component.html.erb +1 -0
- data/app/components/panda/core/admin/tag_component.rb +51 -20
- data/app/components/panda/core/admin/tag_input_component.html.erb +37 -0
- data/app/components/panda/core/admin/tag_input_component.rb +22 -0
- data/app/components/panda/core/admin/tags_panel_component.html.erb +8 -0
- data/app/components/panda/core/admin/tags_panel_component.rb +33 -0
- data/app/components/panda/core/admin/user_activity_component.html.erb +15 -0
- data/app/components/panda/core/admin/user_activity_component.rb +9 -22
- data/app/components/panda/core/admin/user_display_component.html.erb +8 -0
- data/app/components/panda/core/admin/user_display_component.rb +19 -22
- data/app/components/panda/core/base.rb +35 -42
- data/app/components/panda/core/shared/footer_component.html.erb +2 -0
- data/app/components/panda/core/shared/footer_component.rb +11 -0
- data/app/components/panda/core/shared/header_component.html.erb +32 -0
- data/app/components/panda/core/shared/header_component.rb +19 -0
- data/app/constraints/panda/core/admin_constraint.rb +2 -2
- data/app/controllers/concerns/panda/core/admin_authorization.rb +58 -0
- data/app/controllers/concerns/panda/core/authorizable.rb +107 -0
- data/app/controllers/panda/core/admin/base_controller.rb +62 -3
- data/app/controllers/panda/core/admin/editor/search_controller.rb +17 -0
- data/app/controllers/panda/core/admin/feature_flags_controller.rb +35 -0
- data/app/controllers/panda/core/admin/file_categories_controller.rb +84 -0
- data/app/controllers/panda/core/admin/files_controller.rb +230 -0
- data/app/controllers/panda/core/admin/import_sessions_controller.rb +120 -0
- data/app/controllers/panda/core/admin/my_profile/logins_controller.rb +24 -0
- data/app/controllers/panda/core/admin/my_profile_controller.rb +31 -0
- data/app/controllers/panda/core/admin/search_controller.rb +15 -0
- data/app/controllers/panda/core/admin/sessions_controller.rb +84 -10
- data/app/controllers/panda/core/admin/tags_controller.rb +77 -0
- data/app/controllers/panda/core/admin/test_sessions_controller.rb +5 -6
- data/app/controllers/panda/core/admin/users_controller.rb +184 -0
- data/app/controllers/panda/core/application_controller.rb +13 -1
- data/app/helpers/panda/core/active_storage_helper.rb +24 -0
- data/app/helpers/panda/core/feature_flag_helper.rb +11 -0
- data/app/helpers/panda/core/form_helper.rb +15 -0
- data/app/javascript/panda/core/controllers/avatar_upload_controller.js +73 -0
- data/app/javascript/panda/core/controllers/clipboard_controller.js +119 -0
- data/app/javascript/panda/core/controllers/collapsible_item_controller.js +45 -0
- data/app/javascript/panda/core/controllers/custom_select_controller.js +256 -0
- data/app/javascript/panda/core/controllers/datepicker_controller.js +121 -0
- data/app/javascript/panda/core/controllers/daterange_controller.js +193 -0
- data/app/javascript/panda/core/controllers/file_gallery_controller.js +134 -0
- data/app/javascript/panda/core/controllers/file_upload_controller.js +165 -0
- data/app/javascript/panda/core/controllers/global_search_controller.js +184 -0
- data/app/javascript/panda/core/controllers/index.js +36 -0
- data/app/javascript/panda/core/controllers/mobile_sidebar_controller.js +58 -0
- data/app/javascript/panda/core/controllers/navigation_toggle_controller.js +49 -24
- data/app/javascript/panda/core/controllers/row_link_controller.js +27 -0
- data/app/javascript/panda/core/controllers/tag_input_controller.js +183 -0
- data/app/javascript/panda/core/tailwindcss-stimulus-components.js +2 -3
- data/app/javascript/panda/core/vendor/@fortawesome--fontawesome-free@7.2.0.js +8129 -0
- data/app/javascript/panda/core/vendor/@hotwired--turbo.js +7197 -100
- data/app/javascript/panda/core/vendor/@rails--actioncable--src.js +511 -2
- data/app/javascript/panda/core/vendor/@tailwindplus--elements@1.0.22.js +65 -0
- data/app/javascript/panda/core/vendor/cropperjs@2.1.0.js +3 -0
- data/app/javascript/panda/core/vendor/vanilla-calendar-pro.js +2 -0
- data/app/models/concerns/panda/core/has_metadata.rb +164 -0
- data/app/models/concerns/panda/core/importable.rb +79 -0
- data/app/models/concerns/panda/core/searchable.rb +66 -0
- data/app/models/concerns/panda/core/taggable.rb +37 -0
- data/app/models/panda/core/feature_flag.rb +56 -0
- data/app/models/panda/core/file_categorization.rb +14 -0
- data/app/models/panda/core/file_category.rb +45 -0
- data/app/models/panda/core/import_session.rb +79 -0
- data/app/models/panda/core/presence.rb +24 -0
- data/app/models/panda/core/tag.rb +33 -0
- data/app/models/panda/core/tagging.rb +14 -0
- data/app/models/panda/core/user.rb +94 -16
- data/app/models/panda/core/user_activity.rb +42 -0
- data/app/models/panda/core/user_session.rb +35 -0
- data/app/services/panda/core/attach_avatar_service.rb +15 -3
- data/app/services/panda/core/csv_import_service.rb +65 -0
- data/app/services/panda/core/file_categorizer.rb +52 -0
- data/app/services/panda/core/file_parser.rb +76 -0
- data/app/services/panda/core/invite_user_service.rb +44 -0
- data/app/services/panda/core/presence_service.rb +49 -0
- data/app/views/layouts/panda/core/admin.html.erb +6 -76
- data/app/views/layouts/panda/core/admin_simple.html.erb +2 -2
- data/app/views/panda/core/admin/dashboard/_default_content.html.erb +1 -73
- data/app/views/panda/core/admin/dashboard/show.html.erb +12 -15
- data/app/views/panda/core/admin/feature_flags/index.html.erb +56 -0
- data/app/views/panda/core/admin/file_categories/_form.html.erb +20 -0
- data/app/views/panda/core/admin/file_categories/edit.html.erb +5 -0
- data/app/views/panda/core/admin/file_categories/index.html.erb +55 -0
- data/app/views/panda/core/admin/file_categories/new.html.erb +5 -0
- data/app/views/panda/core/admin/files/_file_details.html.erb +156 -0
- data/app/views/panda/core/admin/files/index.html.erb +61 -0
- data/app/views/panda/core/admin/import_sessions/column_map.html.erb +43 -0
- data/app/views/panda/core/admin/import_sessions/index.html.erb +48 -0
- data/app/views/panda/core/admin/import_sessions/new.html.erb +37 -0
- data/app/views/panda/core/admin/import_sessions/preview.html.erb +51 -0
- data/app/views/panda/core/admin/import_sessions/show.html.erb +87 -0
- data/app/views/panda/core/admin/my_profile/edit.html.erb +51 -39
- data/app/views/panda/core/admin/my_profile/logins/show.html.erb +72 -0
- data/app/views/panda/core/admin/sessions/new.html.erb +1 -37
- data/app/views/panda/core/admin/shared/_breadcrumbs.html.erb +1 -18
- data/app/views/panda/core/admin/shared/_sidebar.html.erb +1 -99
- data/app/views/panda/core/admin/shared/_slideover.html.erb +3 -3
- data/app/views/panda/core/admin/tags/_form.html.erb +21 -0
- data/app/views/panda/core/admin/tags/edit.html.erb +4 -0
- data/app/views/panda/core/admin/tags/index.html.erb +41 -0
- data/app/views/panda/core/admin/tags/new.html.erb +4 -0
- data/app/views/panda/core/admin/users/activity.html.erb +90 -0
- data/app/views/panda/core/admin/users/edit.html.erb +48 -0
- data/app/views/panda/core/admin/users/index.html.erb +161 -0
- data/app/views/panda/core/admin/users/sessions.html.erb +103 -0
- data/app/views/panda/core/admin/users/show.html.erb +224 -0
- data/app/views/panda/core/shared/_footer.html.erb +1 -2
- data/app/views/panda/core/shared/_header.html.erb +4 -22
- data/config/brakeman.ignore +68 -31
- data/config/importmap.rb +13 -11
- data/config/routes.rb +49 -2
- data/db/migrate/20260202171614_create_panda_core_file_categories.rb +30 -0
- data/db/migrate/20260203000001_add_user_management_fields_to_panda_core_users.rb +21 -0
- data/db/migrate/20260203000002_create_panda_core_user_activities.rb +24 -0
- data/db/migrate/20260203000003_create_panda_core_user_sessions.rb +25 -0
- data/db/migrate/20260207100000_create_panda_core_presences.rb +21 -0
- data/db/migrate/20260213000001_fix_active_storage_attachments_record_id_type.rb +44 -0
- data/db/migrate/20260217000001_create_panda_core_feature_flags.rb +14 -0
- data/db/migrate/20260303000001_add_metadata_to_panda_core_users.rb +11 -0
- data/db/migrate/20260310000001_create_panda_core_tags.rb +27 -0
- data/db/migrate/20260310000002_create_panda_core_import_sessions.rb +29 -0
- data/lib/generators/panda/core/install_generator.rb +32 -0
- data/lib/generators/panda/core/templates/panda.rb +63 -0
- data/lib/panda/core/asset_loader.rb +5 -2
- data/lib/panda/core/authentication.rb +3 -3
- data/lib/panda/core/configuration.rb +195 -1
- data/lib/panda/core/engine/admin_controller_config.rb +3 -6
- data/lib/panda/core/engine/autoload_config.rb +21 -9
- data/lib/panda/core/engine/omniauth_config.rb +108 -38
- data/lib/panda/core/engine/pending_migrations_check.rb +46 -0
- data/lib/panda/core/engine/sqlite_schema_compatibility.rb +36 -0
- data/lib/panda/core/engine/{phlex_config.rb → view_component_config.rb} +5 -7
- data/lib/panda/core/engine.rb +90 -25
- data/lib/panda/core/middleware.rb +35 -0
- data/lib/panda/core/navigation_registry.rb +455 -0
- data/lib/panda/core/oauth_providers.rb +27 -0
- data/lib/panda/core/permission_registry.rb +89 -0
- data/lib/panda/core/search_registry.rb +69 -0
- data/lib/panda/core/seeds/file_categories.rb +30 -0
- data/lib/panda/core/shared/inflections_config.rb +1 -5
- data/lib/panda/core/testing/rails_helper.rb +87 -0
- data/lib/panda/core/testing/support/authentication_test_helpers.rb +22 -18
- data/lib/panda/core/testing/support/system/cuprite_helpers.rb +4 -4
- data/lib/panda/core/testing/view_component_test_controller.rb +19 -0
- data/lib/panda/core/version.rb +1 -1
- data/lib/panda/core/widget_registry.rb +50 -0
- data/lib/panda/core.rb +17 -8
- data/lib/tasks/panda/core/file_categories.rake +41 -0
- data/public/panda-core-assets/panda-core-0.12.5.css +2 -0
- data/public/panda-core-assets/panda-core-0.13.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.0.css +2 -0
- data/public/panda-core-assets/panda-core-0.14.4.css +2 -0
- data/public/panda-core-assets/panda-core.css +2 -2
- data/public/panda-core-assets/vanilla-calendar.css +47 -0
- metadata +203 -26
- data/lib/panda/core/engine/inflections_config.rb +0 -23
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
module Panda
|
|
2
2
|
module Core
|
|
3
3
|
module OAuthProviders
|
|
4
|
+
# Provider metadata for display purposes
|
|
5
|
+
PROVIDER_INFO = {
|
|
6
|
+
microsoft_graph: {
|
|
7
|
+
name: "Microsoft",
|
|
8
|
+
icon: "fa-brands fa-microsoft",
|
|
9
|
+
color: "#00a4ef"
|
|
10
|
+
},
|
|
11
|
+
google_oauth2: {
|
|
12
|
+
name: "Google",
|
|
13
|
+
icon: "fa-brands fa-google",
|
|
14
|
+
color: "#4285f4"
|
|
15
|
+
},
|
|
16
|
+
github: {
|
|
17
|
+
name: "GitHub",
|
|
18
|
+
icon: "fa-brands fa-github",
|
|
19
|
+
color: "#333"
|
|
20
|
+
}
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
4
23
|
def self.setup
|
|
5
24
|
providers = []
|
|
6
25
|
|
|
@@ -27,6 +46,14 @@ module Panda
|
|
|
27
46
|
|
|
28
47
|
providers
|
|
29
48
|
end
|
|
49
|
+
|
|
50
|
+
def self.info(provider)
|
|
51
|
+
PROVIDER_INFO[provider.to_sym] || {
|
|
52
|
+
name: provider.to_s.titleize,
|
|
53
|
+
icon: "fa-solid fa-circle-user",
|
|
54
|
+
color: "#6b7280"
|
|
55
|
+
}
|
|
56
|
+
end
|
|
30
57
|
end
|
|
31
58
|
end
|
|
32
59
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
# Registry for controller-level permission requirements.
|
|
6
|
+
#
|
|
7
|
+
# Gems and host apps register which permissions are needed for each
|
|
8
|
+
# controller action. The {AdminAuthorization} concern reads this registry
|
|
9
|
+
# at request time to enforce access control.
|
|
10
|
+
#
|
|
11
|
+
# == Usage
|
|
12
|
+
#
|
|
13
|
+
# # In a gem engine (config.to_prepare or initializer):
|
|
14
|
+
# Panda::Core::PermissionRegistry.register(
|
|
15
|
+
# "Panda::CMS::Admin::PagesController",
|
|
16
|
+
# index: :edit_content,
|
|
17
|
+
# edit: :edit_content,
|
|
18
|
+
# destroy: :delete_content
|
|
19
|
+
# )
|
|
20
|
+
#
|
|
21
|
+
# # In a host app initializer:
|
|
22
|
+
# Panda::Core::PermissionRegistry.register(
|
|
23
|
+
# "Admin::HealthcareProvidersController",
|
|
24
|
+
# index: :manage_providers,
|
|
25
|
+
# destroy: :manage_providers
|
|
26
|
+
# )
|
|
27
|
+
#
|
|
28
|
+
# Actions not registered are allowed for any authenticated admin user.
|
|
29
|
+
# Controllers not registered are also allowed (open by default).
|
|
30
|
+
#
|
|
31
|
+
# == How enforcement works
|
|
32
|
+
#
|
|
33
|
+
# The {AdminAuthorization} concern calls {.permission_for} to look up
|
|
34
|
+
# the required permission, then delegates to +authorize!+ (from
|
|
35
|
+
# {Authorizable}) which checks +config.authorization_policy+.
|
|
36
|
+
#
|
|
37
|
+
# Without panda-cms-pro, the default policy is +user.admin?+ — so only
|
|
38
|
+
# admins pass permission checks. With Pro loaded, the policy uses the
|
|
39
|
+
# role system, allowing non-admin users with the right permissions.
|
|
40
|
+
class PermissionRegistry
|
|
41
|
+
class << self
|
|
42
|
+
# Register permission requirements for a controller.
|
|
43
|
+
#
|
|
44
|
+
# Merges with any previously registered permissions for the same
|
|
45
|
+
# controller, so multiple gems can contribute to the same controller.
|
|
46
|
+
#
|
|
47
|
+
# @param controller_name [String] Fully-qualified controller class name
|
|
48
|
+
# @param permissions [Hash{Symbol => Symbol}] action → permission_key mapping
|
|
49
|
+
def register(controller_name, **permissions)
|
|
50
|
+
registry[controller_name] = (registry[controller_name] || {}).merge(permissions)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Look up the required permission for a specific controller action.
|
|
54
|
+
#
|
|
55
|
+
# @param controller_name [String] Fully-qualified controller class name
|
|
56
|
+
# @param action [Symbol] The controller action name
|
|
57
|
+
# @return [Symbol, nil] The required permission key, or nil if not registered
|
|
58
|
+
def permission_for(controller_name, action)
|
|
59
|
+
registry.dig(controller_name, action)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Return all registered permissions for a controller.
|
|
63
|
+
#
|
|
64
|
+
# @param controller_name [String] Fully-qualified controller class name
|
|
65
|
+
# @return [Hash{Symbol => Symbol}, nil] action → permission_key mapping
|
|
66
|
+
def permissions_for(controller_name)
|
|
67
|
+
registry[controller_name]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Return the full registry (deep copy).
|
|
71
|
+
# @return [Hash]
|
|
72
|
+
def all
|
|
73
|
+
registry.transform_values(&:dup)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Clear all registrations (for test isolation).
|
|
77
|
+
def reset!
|
|
78
|
+
@registry = {}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def registry
|
|
84
|
+
@registry ||= {}
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
class SearchRegistry
|
|
6
|
+
@providers = []
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_reader :providers
|
|
10
|
+
|
|
11
|
+
# Register a search provider (idempotent — overwrites any existing provider with the same name)
|
|
12
|
+
# @param name [String] Provider name (e.g., "pages", "posts")
|
|
13
|
+
# @param search_class [Class] Class that responds to .editor_search(query, limit:)
|
|
14
|
+
# and optionally .admin_search(query, limit:) for richer global search results
|
|
15
|
+
def register(name:, search_class:)
|
|
16
|
+
@providers.reject! { |p| p[:name] == name }
|
|
17
|
+
@providers << {name: name, search_class: search_class}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Search all registered providers (editor format)
|
|
21
|
+
# @param query [String] Search query
|
|
22
|
+
# @param limit [Integer] Max results per provider
|
|
23
|
+
# @return [Array<Hash>] Array of { href:, name:, description: }
|
|
24
|
+
def search(query, limit: 5)
|
|
25
|
+
return [] if query.blank?
|
|
26
|
+
|
|
27
|
+
@providers.flat_map do |provider|
|
|
28
|
+
provider[:search_class].editor_search(query, limit: limit)
|
|
29
|
+
rescue => e
|
|
30
|
+
Rails.logger.warn("[panda-core] Search provider #{provider[:name]} failed: #{e.message}") if defined?(Rails.logger)
|
|
31
|
+
[]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Search all registered providers (admin format with grouping)
|
|
36
|
+
# @param query [String] Search query
|
|
37
|
+
# @param limit [Integer] Max results per provider
|
|
38
|
+
# @return [Hash] { groups: [{ name:, icon:, results: [{ href:, name:, description: }] }] }
|
|
39
|
+
def admin_search(query, limit: 5)
|
|
40
|
+
return {groups: []} if query.blank?
|
|
41
|
+
|
|
42
|
+
groups = @providers.filter_map do |provider|
|
|
43
|
+
klass = provider[:search_class]
|
|
44
|
+
next unless klass.respond_to?(:admin_search)
|
|
45
|
+
|
|
46
|
+
results = klass.admin_search(query, limit: limit)
|
|
47
|
+
next if results.empty?
|
|
48
|
+
|
|
49
|
+
config = klass.respond_to?(:searchable_config) ? klass.searchable_config : {}
|
|
50
|
+
{
|
|
51
|
+
name: config[:group] || provider[:name].to_s.titleize,
|
|
52
|
+
icon: config[:icon],
|
|
53
|
+
results: results
|
|
54
|
+
}
|
|
55
|
+
rescue => e
|
|
56
|
+
Rails.logger.warn("[panda-core] Admin search provider #{provider[:name]} failed: #{e.message}") if defined?(Rails.logger)
|
|
57
|
+
nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{groups: groups}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reset!
|
|
64
|
+
@providers = []
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
module Seeds
|
|
6
|
+
module FileCategories
|
|
7
|
+
DEFAULTS = [
|
|
8
|
+
{name: "Media Library", slug: "media-library", icon: "fa-solid fa-photo-film", position: 0},
|
|
9
|
+
{name: "Page Images", slug: "page-images", icon: "fa-solid fa-file-image", position: 1},
|
|
10
|
+
{name: "Post Images", slug: "post-images", icon: "fa-solid fa-newspaper", position: 2},
|
|
11
|
+
{name: "User Avatars", slug: "user-avatars", icon: "fa-solid fa-user-circle", position: 3},
|
|
12
|
+
{name: "Website Avatars", slug: "website-avatars", icon: "fa-solid fa-users", position: 4},
|
|
13
|
+
{name: "Form Uploads", slug: "form-uploads", icon: "fa-solid fa-file-arrow-up", position: 5},
|
|
14
|
+
{name: "Social Media", slug: "social-media", icon: "fab fa-instagram", position: 6}
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def self.seed!
|
|
18
|
+
DEFAULTS.each do |attrs|
|
|
19
|
+
Panda::Core::FileCategory.find_or_create_by!(slug: attrs[:slug]) do |category|
|
|
20
|
+
category.name = attrs[:name]
|
|
21
|
+
category.icon = attrs[:icon]
|
|
22
|
+
category.position = attrs[:position]
|
|
23
|
+
category.system = true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -9,13 +9,9 @@ module Panda
|
|
|
9
9
|
extend ActiveSupport::Concern
|
|
10
10
|
|
|
11
11
|
included do
|
|
12
|
-
# Load inflections early to ensure proper constant resolution
|
|
13
12
|
initializer "panda.inflections", before: :load_config_initializers do
|
|
14
13
|
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
15
|
-
inflect.acronym
|
|
16
|
-
inflect.acronym "SEO"
|
|
17
|
-
inflect.acronym "AI"
|
|
18
|
-
inflect.acronym "UUID"
|
|
14
|
+
Panda::Core::ACRONYMS.each { |acronym| inflect.acronym acronym }
|
|
19
15
|
end
|
|
20
16
|
end
|
|
21
17
|
end
|
|
@@ -16,6 +16,32 @@ require "shoulda/matchers"
|
|
|
16
16
|
require "capybara"
|
|
17
17
|
require "capybara/rspec"
|
|
18
18
|
require "puma"
|
|
19
|
+
require "action_controller/test_case"
|
|
20
|
+
require "view_component/test_helpers"
|
|
21
|
+
require_relative "view_component_test_controller"
|
|
22
|
+
|
|
23
|
+
# Configure ViewComponent to use our test controller
|
|
24
|
+
if defined?(ViewComponent) && Rails.application && Rails.application.config.respond_to?(:view_component)
|
|
25
|
+
Rails.application.config.view_component.test_controller = "ViewComponentTestController"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Ensure test schema is up to date, auto-registering engine migration versions
|
|
29
|
+
# that db:schema:load doesn't track. When schema:load creates tables from
|
|
30
|
+
# schema.rb, it only records the max version in schema_migrations. Engine
|
|
31
|
+
# migrations appear as "pending" even though their columns already exist.
|
|
32
|
+
begin
|
|
33
|
+
ActiveRecord::Migration.maintain_test_schema!
|
|
34
|
+
rescue ActiveRecord::PendingMigrationError
|
|
35
|
+
sm = ActiveRecord::SchemaMigration.new(ActiveRecord::Base.connection_pool)
|
|
36
|
+
existing = sm.versions.map(&:to_i).to_set
|
|
37
|
+
migrator = ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths)
|
|
38
|
+
missing = migrator.migrations.map(&:version).reject { |v| existing.include?(v) }
|
|
39
|
+
if missing.any?
|
|
40
|
+
missing.each { |v| sm.create_version(v.to_s) }
|
|
41
|
+
else
|
|
42
|
+
raise
|
|
43
|
+
end
|
|
44
|
+
end
|
|
19
45
|
|
|
20
46
|
# Configure fixtures
|
|
21
47
|
ActiveRecord::FixtureSet.context_class.send :include, ActiveSupport::Testing::TimeHelpers
|
|
@@ -75,6 +101,13 @@ RSpec.configure do |config|
|
|
|
75
101
|
config.include Rails::Controller::Testing::Integration, type: :controller
|
|
76
102
|
end
|
|
77
103
|
|
|
104
|
+
# ViewComponent testing support (if view_component is available)
|
|
105
|
+
if defined?(ViewComponent::TestHelpers)
|
|
106
|
+
config.include ViewComponent::TestHelpers, type: :component
|
|
107
|
+
config.include Capybara::RSpecMatchers, type: :component
|
|
108
|
+
config.include Panda::Core::AssetHelper, type: :component
|
|
109
|
+
end
|
|
110
|
+
|
|
78
111
|
# Reset column information before suite
|
|
79
112
|
config.before(:suite) do
|
|
80
113
|
if defined?(Panda::Core::User)
|
|
@@ -163,6 +196,60 @@ RSpec.configure do |config|
|
|
|
163
196
|
# OmniAuth test mode
|
|
164
197
|
OmniAuth.config.test_mode = true if defined?(OmniAuth)
|
|
165
198
|
|
|
199
|
+
# Stub Panda Core helpers for component tests
|
|
200
|
+
config.before(:each, type: :component) do
|
|
201
|
+
if defined?(ViewComponent::TestHelpers)
|
|
202
|
+
allow_any_instance_of(ActionView::Base).to receive(:panda_core_stylesheet).and_return("")
|
|
203
|
+
allow_any_instance_of(ActionView::Base).to receive(:panda_core_javascript).and_return("")
|
|
204
|
+
allow_any_instance_of(ActionView::Base).to receive(:csrf_meta_tags).and_return("")
|
|
205
|
+
allow_any_instance_of(ActionView::Base).to receive(:csp_meta_tag).and_return("")
|
|
206
|
+
allow_any_instance_of(ActionView::Base).to receive(:controller).and_return(
|
|
207
|
+
double(
|
|
208
|
+
class: double(name: "Test"),
|
|
209
|
+
protect_against_forgery?: false,
|
|
210
|
+
content_security_policy?: false
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Stub link_to and button_to to avoid routing complexity in component tests
|
|
215
|
+
allow_any_instance_of(ActionView::Base).to receive(:link_to) do |*args, &block|
|
|
216
|
+
href = args[0].is_a?(Hash) ? "#" : args[0]
|
|
217
|
+
options = args.find { |a| a.is_a?(Hash) } || {}
|
|
218
|
+
css_class = options[:class] || ""
|
|
219
|
+
content = block ? block.call : args[1] || href
|
|
220
|
+
"<a href=\"#{href}\" class=\"#{css_class}\">#{content}</a>".html_safe
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
allow_any_instance_of(ActionView::Base).to receive(:button_to) do |*args, &block|
|
|
224
|
+
args[0]
|
|
225
|
+
options = args[1] || {}
|
|
226
|
+
css_class = options[:class] || ""
|
|
227
|
+
content = block ? block.call : "Button"
|
|
228
|
+
id_attr = options[:id] ? " id=\"#{options[:id]}\"" : ""
|
|
229
|
+
"<button class=\"#{css_class}\"#{id_attr}>#{content}</button>".html_safe
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Stub panda_core routes to return simple paths
|
|
233
|
+
panda_core_routes = double("panda_core_routes")
|
|
234
|
+
allow(panda_core_routes).to receive(:admin_logout_path).and_return("/admin/logout")
|
|
235
|
+
allow(panda_core_routes).to receive(:admin_my_profile_path).and_return("/admin/my_profile")
|
|
236
|
+
allow_any_instance_of(ActionView::Base).to receive(:panda_core).and_return(panda_core_routes)
|
|
237
|
+
|
|
238
|
+
# Stub current_user
|
|
239
|
+
mock_user = double(
|
|
240
|
+
"User",
|
|
241
|
+
name: "Test User",
|
|
242
|
+
avatar: double(attached?: false),
|
|
243
|
+
image_url: ""
|
|
244
|
+
)
|
|
245
|
+
allow_any_instance_of(ActionView::Base).to receive(:current_user).and_return(mock_user)
|
|
246
|
+
|
|
247
|
+
# Stub main_app routes
|
|
248
|
+
main_app_routes = double(url_for: "/test")
|
|
249
|
+
allow_any_instance_of(ActionView::Base).to receive(:main_app).and_return(main_app_routes)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
166
253
|
# DatabaseCleaner configuration
|
|
167
254
|
config.around(:each) do |example|
|
|
168
255
|
DatabaseCleaner.strategy = (example.metadata[:type] == :system) ? :truncation : :transaction
|
|
@@ -18,30 +18,34 @@ module Panda
|
|
|
18
18
|
def create_admin_user(attributes = {})
|
|
19
19
|
ensure_columns_loaded
|
|
20
20
|
admin_id = "8f481fcb-d9c8-55d7-ba17-5ea5d9ed8b7a"
|
|
21
|
-
Panda::Core::User.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
user = Panda::Core::User.find_or_initialize_by(id: admin_id)
|
|
22
|
+
user.email = attributes[:email] || "admin@example.com"
|
|
23
|
+
user.name = attributes[:name] || "Admin User"
|
|
24
|
+
user.image_url = attributes[:image_url] || default_image_url
|
|
25
|
+
user.admin = attributes.fetch(:admin, true)
|
|
26
|
+
user.enabled = attributes.fetch(:enabled, true) if user.respond_to?(:enabled=)
|
|
27
|
+
# Only set OAuth fields if they exist on the model
|
|
28
|
+
user.uid = attributes[:uid] || "admin_oauth_uid_123" if user.respond_to?(:uid=)
|
|
29
|
+
user.provider = attributes[:provider] || "google_oauth2" if user.respond_to?(:provider=)
|
|
30
|
+
user.save! if user.new_record? || user.changed?
|
|
31
|
+
user
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
# Create a regular user with fixed ID for consistent fixture references
|
|
33
35
|
def create_regular_user(attributes = {})
|
|
34
36
|
ensure_columns_loaded
|
|
35
37
|
regular_id = "9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d"
|
|
36
|
-
Panda::Core::User.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
38
|
+
user = Panda::Core::User.find_or_initialize_by(id: regular_id)
|
|
39
|
+
user.email = attributes[:email] || "user@example.com"
|
|
40
|
+
user.name = attributes[:name] || "Regular User"
|
|
41
|
+
user.image_url = attributes[:image_url] || default_image_url(dark: true)
|
|
42
|
+
user.admin = attributes.fetch(:admin, false)
|
|
43
|
+
user.enabled = attributes.fetch(:enabled, true) if user.respond_to?(:enabled=)
|
|
44
|
+
# Only set OAuth fields if they exist on the model
|
|
45
|
+
user.uid = attributes[:uid] || "user_oauth_uid_456" if user.respond_to?(:uid=)
|
|
46
|
+
user.provider = attributes[:provider] || "google_oauth2" if user.respond_to?(:provider=)
|
|
47
|
+
user.save! if user.new_record? || user.changed?
|
|
48
|
+
user
|
|
45
49
|
end
|
|
46
50
|
|
|
47
51
|
# Backwards compatibility with fixture access patterns
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "shellwords"
|
|
4
4
|
|
|
5
5
|
# Helper methods for Cuprite-based system tests
|
|
6
6
|
#
|
|
@@ -74,9 +74,9 @@ module Panda
|
|
|
74
74
|
File.binwrite(File.join(png_dir, "frame-%05d.png" % i), Base64.decode64(data))
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
escaped_png_dir = Shellwords.escape(File.join(png_dir, "*.png"))
|
|
78
|
+
escaped_path = Shellwords.escape(path.to_s)
|
|
79
|
+
system("ffmpeg -y -framerate 8 -pattern_type glob -i #{escaped_png_dir} -c:v libx264 -pix_fmt yuv420p #{escaped_path}")
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
puts "🎥 Saved video: #{path}"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Simple controller for ViewComponent testing
|
|
4
|
+
class ViewComponentTestController < ActionController::Base
|
|
5
|
+
# Disable CSRF protection for test controller
|
|
6
|
+
# This is intentional as this controller is only used for component testing
|
|
7
|
+
protect_from_forgery with: :null_session
|
|
8
|
+
|
|
9
|
+
# Include necessary helpers
|
|
10
|
+
include Rails.application.routes.url_helpers if defined?(Rails.application)
|
|
11
|
+
|
|
12
|
+
# Set up a basic layout
|
|
13
|
+
layout false
|
|
14
|
+
|
|
15
|
+
# Make helpers available
|
|
16
|
+
def helpers
|
|
17
|
+
ActionController::Base.helpers
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/panda/core/version.rb
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Panda
|
|
4
|
+
module Core
|
|
5
|
+
class WidgetRegistry
|
|
6
|
+
@widgets = []
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
attr_reader :widgets
|
|
10
|
+
|
|
11
|
+
# Register a dashboard widget.
|
|
12
|
+
# Duplicate labels are replaced (last registration wins).
|
|
13
|
+
# @param label [String] Widget label (used for identification and deduplication)
|
|
14
|
+
# @param component [Proc] Lambda receiving user, returns an instantiated component
|
|
15
|
+
# @param visible [Proc, nil] Lambda receiving user, hides widget when false
|
|
16
|
+
# @param position [Integer] Sort order (lower numbers appear first)
|
|
17
|
+
def register(label, component:, visible: nil, position: 0)
|
|
18
|
+
widget = {label: label, component: component, visible: visible, position: position}
|
|
19
|
+
if (index = @widgets.index { |w| w[:label] == label })
|
|
20
|
+
@widgets[index] = widget
|
|
21
|
+
else
|
|
22
|
+
@widgets << widget
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Build the widget list for the current user.
|
|
27
|
+
# Combines the legacy admin_dashboard_widgets lambda with registered widgets.
|
|
28
|
+
# @param user [Object] The current user
|
|
29
|
+
# @return [Array] Instantiated widget components
|
|
30
|
+
def build(user)
|
|
31
|
+
# Legacy backward-compatible widgets from the lambda
|
|
32
|
+
legacy = Panda::Core.config.admin_dashboard_widgets&.call(user) || []
|
|
33
|
+
|
|
34
|
+
# Registered widgets — filter by visibility, sort by position, instantiate
|
|
35
|
+
registered = @widgets
|
|
36
|
+
.select { |w| w[:visible].nil? || w[:visible].call(user) }
|
|
37
|
+
.sort_by { |w| w[:position] }
|
|
38
|
+
.map { |w| w[:component].call(user) }
|
|
39
|
+
|
|
40
|
+
legacy + registered
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Clear all registrations (for test isolation).
|
|
44
|
+
def reset!
|
|
45
|
+
@widgets = []
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/panda/core.rb
CHANGED
|
@@ -4,18 +4,19 @@ require "rails"
|
|
|
4
4
|
# require "active_support/core_ext/module/attribute_accessors"
|
|
5
5
|
|
|
6
6
|
# Load inflections early so they're available for generators
|
|
7
|
-
# This must happen before any code tries to camelize/classify strings
|
|
8
|
-
# containing "cms", "seo", "ai", or "uuid"
|
|
9
7
|
require "active_support/inflector"
|
|
10
|
-
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
11
|
-
inflect.acronym "CMS"
|
|
12
|
-
inflect.acronym "SEO"
|
|
13
|
-
inflect.acronym "AI"
|
|
14
|
-
inflect.acronym "UUID"
|
|
15
|
-
end
|
|
16
8
|
|
|
17
9
|
module Panda
|
|
18
10
|
module Core
|
|
11
|
+
# Single source of truth for acronyms used across all Panda gems.
|
|
12
|
+
# Referenced by Shared::InflectionsConfig (engine initializer) and
|
|
13
|
+
# applied eagerly below for generator/CLI contexts.
|
|
14
|
+
ACRONYMS = %w[CMS CSV SEO AI URL UUID].freeze
|
|
15
|
+
|
|
16
|
+
# Session key for storing authenticated Panda Core admin user ID
|
|
17
|
+
# Namespaced to avoid conflicts with host application user sessions
|
|
18
|
+
ADMIN_SESSION_KEY = :panda_core_user_id
|
|
19
|
+
|
|
19
20
|
def self.root
|
|
20
21
|
File.expand_path("../..", __FILE__)
|
|
21
22
|
end
|
|
@@ -25,8 +26,16 @@ module Panda
|
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
|
|
29
|
+
# Apply acronyms eagerly for generators and CLI contexts
|
|
30
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
31
|
+
Panda::Core::ACRONYMS.each { |acronym| inflect.acronym acronym }
|
|
32
|
+
end
|
|
33
|
+
|
|
28
34
|
require_relative "core/version"
|
|
29
35
|
require_relative "core/configuration"
|
|
36
|
+
require_relative "core/navigation_registry"
|
|
37
|
+
require_relative "core/permission_registry"
|
|
38
|
+
require_relative "core/widget_registry"
|
|
30
39
|
require_relative "core/asset_loader"
|
|
31
40
|
require_relative "core/debug"
|
|
32
41
|
require_relative "core/services/base_service"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :panda do
|
|
4
|
+
namespace :core do
|
|
5
|
+
namespace :file_categories do
|
|
6
|
+
desc "Seed default file categories"
|
|
7
|
+
task seed: [:environment] do
|
|
8
|
+
require "panda/core/seeds/file_categories"
|
|
9
|
+
Panda::Core::Seeds::FileCategories.seed!
|
|
10
|
+
puts "Default file categories seeded."
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
desc "Categorize existing blobs based on their attachments"
|
|
14
|
+
task categorize_existing: [:environment] do
|
|
15
|
+
require "panda/core/seeds/file_categories"
|
|
16
|
+
Panda::Core::Seeds::FileCategories.seed!
|
|
17
|
+
|
|
18
|
+
categorizer = Panda::Core::FileCategorizer.new
|
|
19
|
+
categorized = 0
|
|
20
|
+
skipped = 0
|
|
21
|
+
|
|
22
|
+
ActiveStorage::Blob.find_each do |blob|
|
|
23
|
+
if Panda::Core::FileCategorization.exists?(blob_id: blob.id)
|
|
24
|
+
skipped += 1
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
attachment = ActiveStorage::Attachment.where(blob_id: blob.id).first
|
|
29
|
+
if attachment
|
|
30
|
+
categorizer.categorize_attachment(attachment)
|
|
31
|
+
else
|
|
32
|
+
categorizer.categorize_blob(blob, category_slug: "media-library")
|
|
33
|
+
end
|
|
34
|
+
categorized += 1
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
puts "Categorized #{categorized} blobs, skipped #{skipped} already-categorized."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|