Skip to content

Dynamic Fields System for Flexible Entity Customization#100

Merged
tarekio merged 491 commits intomainfrom
dynamic-content-fresh
Nov 1, 2025
Merged

Dynamic Fields System for Flexible Entity Customization#100
tarekio merged 491 commits intomainfrom
dynamic-content-fresh

Conversation

@level09
Copy link
Collaborator

@level09 level09 commented Jun 7, 2025

Overview

Implements a comprehensive dynamic fields system that allows administrators to create custom fields for any entity type (bulletins, actors, incidents) without requiring database migrations or code changes. This addresses the need for flexible data collection in bayanat workflows.

Key Features

  • 8 Supported Field Types: string, integer, datetime, array, text, boolean, float, json
  • Real-time Schema Updates: Dynamically creates/drops database columns with proper indexing
  • UI Component Mapping: Automatic mapping from field types to appropriate UI components
  • Type Safety: Comprehensive validation including reserved name checking and type constraints
  • Search Integration: Optional PostgreSQL GIN indexing for searchable fields
  • Multi-entity Support: Works with bulletins, actors, and incidents

Architecture Changes

  • DynamicField Model: Core model for managing field definitions and database schema
  • BaseMixin Enhancement: Automatic integration of dynamic fields into JSON serialization
  • Bulletin Model Updates: Minimal changes to support dynamic fields in existing workflows
  • CLI Commands: Added testing and management commands for dynamic fields

Backward Compatibility

Fully backward compatible - all existing API endpoints, serialization modes, and functionality preserved
Additive only - dynamic fields enhance existing models without breaking changes
Zero migration required - works with existing data and schemas

Testing

  • Comprehensive CLI test command: flask test-dynamic-fields
  • All existing tests continue to pass
  • No breaking changes to existing functionality

Files Changed

  • enferno/admin/models/DynamicField.py - New dynamic field model
  • enferno/utils/base.py - Enhanced BaseMixin with dynamic field support
  • enferno/admin/models/Bulletin.py - Minimal integration changes
  • enferno/commands.py - Added CLI commands for testing
  • enferno/admin/models/__init__.py - Model registration

Usage Example

# Create a custom priority field for bulletins
field = DynamicField(
    name="priority_level",
    title="Priority Level", 
    entity_type="bulletin",
    field_type=DynamicField.STRING,
    ui_component=DynamicField.UIComponent.DROPDOWN,
    options=["High", "Medium", "Low"]
)
field.save()
field.create_column()  # Adds column to bulletins table

Dynamic fields are automatically included in JSON serialization without any additional code changes.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for custom, user-configurable fields on Bulletins, Actors, and Incidents with multiple field types (text, number, select, date/time, etc.)
    • Introduced field management interfaces for admin users to create, edit, reorder, and delete custom fields
    • Added field revision history tracking to audit form configuration changes
    • Extended search functionality to support filtering on custom fields
    • Added field visibility and searchability controls for granular field management
  • UI Enhancements

    • New drag-and-drop field reordering interface
    • Field configuration dialogs with change review and confirmation workflow
    • Integrated custom field rendering across entity detail views

@coderabbitai
Copy link

coderabbitai bot commented Sep 3, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

A comprehensive dynamic field system has been introduced, enabling user-configurable entity fields (bulletin, actor, incident) with runtime CRUD operations, schema validation, search integration, and corresponding admin UI for field management across the application.

Changes

Cohort / File(s) Change Summary
Dynamic Field Core System
enferno/admin/models/DynamicField.py, enferno/admin/models/DynamicFormHistory.py, enferno/admin/models/core_fields.py
New DynamicField model with field taxonomy, validation, column management, entity integration, and value handling; DynamicFormHistory for audit trails; core_fields.py provides seed logic for bulletin/actor/incident core fields.
Entity Model Integration
enferno/admin/models/Bulletin.py, enferno/admin/models/Actor.py, enferno/admin/models/Incident.py
Added dynamic field serialization via DynamicField.apply_values() in from_json and DynamicField.extract_values_for() in to_dict; merged dynamic field values into entity representations.
API & Backend
enferno/admin/views.py, enferno/admin/validation/models.py, enferno/utils/search_utils.py
New endpoints: GET/POST /api/dynamic-fields/, bulk-save, history retrieval; validation support for dynamic fields across query models; search integration via _apply_dynamic_field_filters.
Admin Field Management UI
enferno/admin/templates/admin/bulletin-fields.html, enferno/admin/templates/admin/actor-fields.html, enferno/admin/templates/admin/incident-fields.html
Complete field editor UIs with modal creation, change review dialog, revision history, drag-and-drop reordering, and bulk save workflow for each entity type.
Admin Templates & Dialogs
enferno/admin/templates/admin/bulletins.html, enferno/admin/templates/admin/actors.html, enferno/admin/templates/admin/incidents.html, enferno/admin/templates/admin/partials/bulletin_dialog.html, enferno/admin/templates/admin/partials/incident_dialog.html
Added Edit Form buttons; integrated FieldRenderer component and formBuilderMixin; replaced static field layouts with dynamic field-driven rendering.
Layout & Configuration
enferno/admin/templates/admin/partials/actor_dialog.html, enferno/admin/templates/admin/partials/edit_related_*.html, enferno/admin/templates/admin/system-administration.html
Fixed grid usage; removed margin classes for cleaner layout; updated dialog header text.
Translation Keys
enferno/admin/templates/admin/jsapi.jinja2
Added 50+ field-related translation keys for UI elements (field types, actions, validation messages); renamed setting_/before_/after_ to field_/previous_/updated_.
Vue Components (Field Management)
enferno/static/js/components/FieldListItem.js, enferno/static/js/components/SelectFieldTypeDialog.js, enferno/static/js/components/FieldRenderer.js
New components for field list rendering with editing, field type selection dialog, and dynamic field rendering in forms.
Vue Components (UI Framework)
enferno/static/js/components/ConfirmDialog.js, enferno/static/js/components/Toast.js
Reusable confirm and toast dialogs with Promise-based workflows and customizable props.
Vue Components (Existing Updates)
enferno/static/js/components/EditableTable.js, enferno/static/js/components/GeoMap.js, enferno/static/js/components/UniField.js, enferno/static/js/components/LocationCard.js
Removed globalMixin dependency from EditableTable and GeoMap; added numeric type support to UniField props; enhanced overflow handling in card components.
Vue Mixins
enferno/static/js/mixins/form-builder-mixin.js, enferno/static/js/mixins/global-mixin.js
New form-builder-mixin for comprehensive field lifecycle, drag-and-drop, change tracking, and history management; global-mixin now exposes ConfirmDialog and Toast via $confirm/$toast methods.
Card Components (Dynamic Integration)
enferno/static/js/components/ActorCard.js, enferno/static/js/components/BulletinCard.js, enferno/static/js/components/IncidentCard.js
Replaced static sections with dynamic field-driven rendering; added dynamic field fetch on mount; refactored to conditional field display.
Search Box Components
enferno/static/js/components/ActorSearchBox.js, enferno/static/js/components/BulletinSearchBox.js, enferno/static/js/components/IncidentSearchBox.js
Added dynamic field search support via dyn Map; introduced updateDynamicField/buildAndEmitDyn methods; integrated searchable dynamic fields into filter UI.
Templates & Layout
enferno/templates/layout.html, enferno/admin/templates/admin/bulletins.html, enferno/admin/templates/admin/activity.html, enferno/data_import/templates/import-log.html, enferno/deduplication/templates/deduplication.html, enferno/export/templates/export-dashboard.html, enferno/templates/setup_wizard.html
Added confirm-dialog/toast components to layout; integrated formBuilderMixin into multiple templates; removed mixin scripts from setup_wizard.
Static Assets
enferno/static/css/app.css
Removed chip-container; converted utility classes to CSS variables; added pointer-events-auto, rounded-16/12/10; introduced drop-line and form-grid styles for drag-and-drop UI.
Config & Routing
enferno/static/js/common/config.js
Added design tokens (variables); extended Vuetify themes with core-field-accent color; added field management routes (/admin/bulletin-fields/, etc.).
Package & Models
enferno/admin/models/__init__.py, enferno/utils/base.py
Exported DynamicField and DynamicFormHistory; BaseMixin now includes get_dynamic_fields() and to_dict() for dynamic field support.
Utilities
enferno/utils/dynamic_field_utils.py, enferno/utils/form_history_utils.py, enferno/utils/validation_utils.py, enferno/utils/date_helper.py
New CRUD helpers for dynamic fields; form history recording utility; FieldType enum and validate_field_type for runtime field validation; enhanced parse_datetime with dual-format support.
Database & Commands
enferno/migrations/20250820_101500_create_dynamic_fields_table.sql, enferno/migrations/20251003_144517_seed_incident_core_fields.sql, enferno/commands.py
Created dynamic_fields and dynamic_form_history tables with indexes; added generate_core_fields() and create_initial_snapshots() to DB initialization flow.
Tests & Config
tests/admin/test_dynamic_fields.py, tests/utils/test_search_utils.py, pyproject.toml
Added comprehensive DynamicField model tests and search filter tests; removed numba dependency.

Sequence Diagram

sequenceDiagram
    participant User as User/Admin
    participant UI as Field Editor UI
    participant API as Backend API
    participant DB as Database
    participant EntityModel as Entity Model

    User->>UI: Open field management page
    UI->>API: GET /api/dynamic-fields/?entity_type=bulletin
    API->>DB: Query active fields
    DB-->>API: Return field list + original snapshot
    API-->>UI: Return fields
    UI->>UI: Initialize form with fields

    User->>UI: Add/Edit/Delete field
    UI->>UI: Track changes locally
    
    User->>UI: Click Save Changes
    UI->>UI: Compute diffs (create/update/delete)
    UI->>UI: Show review dialog with changes table
    
    User->>UI: Confirm changes
    UI->>API: POST /api/dynamic-fields/bulk-save
    API->>DB: Begin transaction
    API->>DB: Create/Update/Delete fields
    DB-->>API: Field IDs + results
    API->>DB: Record DynamicFormHistory snapshot
    DB-->>API: History recorded
    API->>DB: Commit transaction
    DB-->>API: Success
    API-->>UI: Return results
    UI->>UI: Re-fetch fields + show success
    UI-->>User: Fields saved successfully

    User->>UI: View revision history
    UI->>API: GET /api/dynamic-fields/history/bulletin
    API->>DB: Query form history (paginated)
    DB-->>API: Return snapshots + metadata
    API-->>UI: Return history
    UI->>UI: Display timeline with per-item changes
Loading
sequenceDiagram
    participant User as Admin/Editor
    participant EntityForm as Bulletin/Actor/Incident Form
    participant FieldRenderer as FieldRenderer Component
    participant DynamicFields as Dynamic Fields System
    participant DB as Database

    User->>EntityForm: Open form to edit entity
    EntityForm->>DynamicFields: fetchDynamicFields({ entityType })
    DynamicFields->>DB: Query active dynamic fields
    DB-->>DynamicFields: Return field configs
    DynamicFields-->>EntityForm: Return fields + current values
    EntityForm->>EntityForm: Populate movableDynamicFields

    EntityForm->>EntityForm: Render form with FieldRenderer per field
    User->>FieldRenderer: Input values in dynamic field
    FieldRenderer->>FieldRenderer: Apply validation rules
    User->>EntityForm: Submit form

    EntityForm->>DB: Apply values via DynamicField.apply_values()
    DB->>DB: Update dynamic field columns (raw SQL)
    DB-->>EntityForm: Values persisted
    
    EntityForm->>DB: Serialize entity via to_dict()
    DB->>DynamicFields: Extract current values via extract_values_for()
    DB-->>DB: Read from dynamic columns
    DB-->>EntityForm: Return merged dict (core + dynamic)
    EntityForm-->>User: Entity saved with dynamic fields
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Areas requiring extra attention:

  • DynamicField model (enferno/admin/models/DynamicField.py): Complex validation logic, SQL column creation/deletion, JSONB config handling, and multi-type support across field types.
  • Bulk save API endpoint (enferno/admin/views.py, api_dynamic_fields_bulk_save): Transaction management, partial failure handling, and history recording logic.
  • Search integration (enferno/utils/search_utils.py, _apply_dynamic_field_filters): Raw SQL generation for multiple field types and operators; potential SQL injection vectors.
  • Entity serialization (Bulletin/Actor/Incident to_dict and from_json): Integration of dynamic fields into existing serialization; order of operations and error handling.
  • Form builder mixin (enferno/static/js/mixins/form-builder-mixin.js): Complex state management, drag-and-drop logic, change detection, and API orchestration; ~500+ lines of interconnected logic.
  • Dynamic field validation (enferno/utils/validation_utils.py, validate_field_type): Type coercion logic for multiple field types; potential edge cases in datetime/numeric parsing.
  • Database migrations: Two migration files with DDL, seeding, and conditional logic; verify index coverage and rollback correctness.
  • Frontend component integration across multiple templates: Risk of missing mixin wiring or component registration; template conditional logic complexity.

🐰 Dynamic fields spring to life,
Core and custom, end the strife!
Build thy forms with drag and drop,
Changes tracked from end to top! 🎨✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request description provides comprehensive and well-structured content covering overview, key features, architecture changes, backward compatibility, testing, and usage examples. However, it deviates significantly from the required template structure by omitting critical sections: no Jira Issue links are provided, the required checklist with test/documentation/translation status is entirely absent, and the API Changes section is not explicitly addressed. While the description content quality is high and informative, it fails to follow the repository's required template format, which specifies mandatory sections for tracking, accountability, and consistency. Reformat the pull request description to follow the required template structure. Add a "Jira Issue" section with relevant issue links, include the "Checklist" section with checked/unchecked boxes for tests, documentation, and translations, and explicitly add an "API Changes" section documenting permissions, endpoint changes, and test coverage. Preserve the detailed content already provided by moving it into the appropriate template sections.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "Dynamic Fields System for Flexible Entity Customization" directly and accurately reflects the primary change in the changeset. It is concise, descriptive, and avoids vague terminology or excessive detail, making it clear to anyone scanning the repository history what the core contribution is. The title aligns well with the comprehensive implementation of the dynamic fields feature described throughout the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 92.63% which is sufficient. The required threshold is 80.00%.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 33

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
enferno/static/js/components/GeoMap.js (2)

41-46: Fix falsy checks that break (0, 0) coordinates

Using truthy checks treats 0 as false, blocking valid coords at the equator/prime meridian. Replace with null checks in all places that gate behavior on lat/lng presence.

Apply this diff:

@@
-      get() {
-        if (this.lat && this.lng) {
+      get() {
+        if (this.lat != null && this.lng != null) {
           return [this.lat, this.lng];
         }
         return geoMapDefaultCenter;
       },
@@
-    if (lat && lng) {
+    if (lat != null && lng != null) {
       this.lat = lat;
       this.lng = lng;
       this.radius = radius;
@@
-    updateMap() {
-      if (this.lat && this.lng && this.map) {
+    updateMap() {
+      if (this.map && this.lat != null && this.lng != null) {
         this.map.setView([this.lat, this.lng]);
         this.setMarker({ latlng: { lat: this.lat, lng: this.lng } });
       } else {
         this.clearMarker();
       }
     },
@@
-    updateRadiusCircle() {
-      if (this.radiusControls && this.lat && this.lng) {
+    updateRadiusCircle() {
+      if (this.radiusControls && this.lat != null && this.lng != null) {
         if (this.radiusCircle) {
           this.map.removeLayer(this.radiusCircle);
         }
         this.radiusCircle = L.circle([this.lat, this.lng], {
           radius: this.radius,
         });
@@
-      if (!this.lat || !this.lng) {
+      if (this.lat == null || this.lng == null) {
         return;
       }

Also applies to: 135-146, 213-219, 221-239, 349-351


247-255: Avoid adding a second base tile layer in fixMap()

You already add osmLayer (and optionally googleLayer) in initMap. Adding another tile layer here duplicates network/rendering work and can overlay tiles.

Apply this diff:

-        if (!this.tileLayer) {
-          this.tileLayer = L.tileLayer(this.mapsApiEndpoint, {
-            attribution: this.attribution,
-          }).addTo(this.map);
-
-          this.tileLayer.on('error', function (error) {
-            console.error('Tile layer error:', error);
-          });
-        }
+        // Only invalidate size; base layers are created in initMap()
enferno/static/js/diff-tool.js (1)

63-84: Escape HTML to prevent XSS in rendered diffs

User-supplied string values are injected into HTML without escaping. Escape strings and generic values before insertion.

     renderDiff: function (diff, labels = {}) {
-        const formatValue = (value, options) => {
+        const escapeHtml = (s) => String(s)
+            .replaceAll('&', '&')
+            .replaceAll('<', '&lt;')
+            .replaceAll('>', '&gt;')
+            .replaceAll('"', '&quot;')
+            .replaceAll("'", '&#39;');
+
+        const formatValue = (value, options) => {
             if (value === undefined || value === null || value === "") return `<span class="font-italic">${window?.translations?.unset_ ?? 'UNSET'}</span>`;
             if (Array.isArray(value)) {
                 return `${value.map(formatValue).join(', ')}`;
             }
             if (typeof value === 'object') {
                 const childEntries = Object.entries(value)
                 
                 return `<ul>${childEntries
                     .map(([key, val], index) => {
                         const isLast = index === childEntries.length - 1
                         const indentClass = options?.hasParent ? 'ml-2' : '';
                         const marginClass = options?.hasParent || isLast ? '' : 'mb-1';
 
-                        return `<li class="${[indentClass, marginClass].filter(Boolean).join(' ')}">${key.toUpperCase()}: ${formatValue(val, { hasParent: true })}</li>`
+                        return `<li class="${[indentClass, marginClass].filter(Boolean).join(' ')}">${escapeHtml(key.toUpperCase())}: ${formatValue(val, { hasParent: true })}</li>`
                     })
                     .join('')}</ul>`;
             }
-            if (typeof value === 'string') return `${value}`;
-            if (typeof value === 'boolean') return value ? `${window?.translations?.on_ ?? 'On'}` : `${window?.translations?.off_ ?? 'Off'}`;
-            return value;
+            if (typeof value === 'string') return `${escapeHtml(value)}`;
+            if (typeof value === 'boolean') return value ? `${escapeHtml(window?.translations?.on_ ?? 'On')}` : `${escapeHtml(window?.translations?.off_ ?? 'Off')}`;
+            return escapeHtml(value);
         };
enferno/admin/views.py (1)

3855-3862: Use abort(description=...) and avoid bare rethrow on parse errors

Prefer abort(400, description=...) and avoid “raise abort(...)”. Also chain or suppress the original ValueError explicitly.

-    except ValueError:
-        raise abort(400, body="Values provided were not in expected format")
+    except ValueError as err:
+        abort(400, description="Values provided were not in expected format")
♻️ Duplicate comments (1)
enferno/admin/views.py (1)

6682-6697: Avoid leaking exception messages to clients

Mirror the pattern above: log exception with stack; return a generic 500 to callers.

-    except Exception as e:
-        # Log the error and return a generic server error response
-        return jsonify({"errors": [{"message": str(e)}]}), 500
+    except Exception:
+        logger.exception("api_dynamic_field failed")
+        return jsonify({"errors": [{"message": "Internal server error"}]}), 500
🧹 Nitpick comments (45)
enferno/admin/templates/admin/partials/edit_related_bulletins.html (1)

1-1: Potential spacing regression: removing my-5 may crowd the dialog

Dropping the vertical margin can compress adjacent sections in incident_dialog. Verify spacing across small/medium breakpoints and consider applying margin at the container level if this was intentional.

enferno/admin/templates/admin/partials/edit_related_incidents.html (1)

1-1: Double-check vertical spacing consistency

Same as related_bulletins: confirm the dialog still has adequate visual separation without my-5, or move spacing to the parent container for consistency.

enferno/static/js/components/FieldRenderer.js (1)

56-63: Dropdowns should default items to [] to avoid runtime warnings

field.options may be undefined; v-select expects an array.

Apply this diff:

-                    items: field.options,
+                    items: Array.isArray(field.options) ? field.options : [],
@@
-                    items: field.options,
+                    items: Array.isArray(field.options) ? field.options : [],

Also applies to: 64-71

enferno/static/js/components/ConfirmDialog.js (3)

16-17: Remove unused reject plumbing; confirm() resolves boolean only

rejectPromise is stored but never used; simplify API and state.

Apply this diff:

       resolvePromise: null,
-      rejectPromise: null,
-      return new Promise((resolve, rejectPromise) => {
-        this.resolvePromise = resolve;
-        this.rejectPromise = rejectPromise;
-      });
+      return new Promise((resolve) => {
+        this.resolvePromise = resolve;
+      });
-        this.resolvePromise = null;
-        this.rejectPromise = null;
+        this.resolvePromise = null;

Also applies to: 42-45, 75-76


63-66: Log acceptance errors as errors

Use console.error for failures; keep UI behavior unchanged.

Apply this diff:

-      } catch (error) {
-        console.log('Something failed when confirming dialog', error)
+      } catch (error) {
+        console.error('Something failed when confirming dialog', error)

7-9: Avoid cleanup race if a new dialog opens within 300ms

The delayed cleanup can clobber props of a newly opened dialog. Track and clear the timeout.

Apply this diff:

       loading: false,
+      cleanupTimer: null,
       dialogProps: {},
-    cleanup() {
-      setTimeout(() => {
+    cleanup() {
+      clearTimeout(this.cleanupTimer);
+      this.cleanupTimer = setTimeout(() => {
         this.loading = false;
         this.onAccept = null;
         this.onReject = null;
         this.resolvePromise = null;
-        this.rejectPromise = null;
         this.cancelProps = { text: 'Cancel', variant: 'outlined' };
         this.acceptProps = { text: 'Confirm', color: 'primary', variant: 'flat' };
         this.dialogProps = {};
-      }, 300)
+      }, 300)
     },

Also applies to: 70-81

enferno/admin/templates/admin/system-administration.html (1)

1018-1027: Vuetify color tokens: use v3-friendly names

Prefer hyphenated tokens for consistency with Vuetify 3 theming (matches existing style if already used elsewhere).

Apply this diff:

-                <div class="d-flex align-center body-2">
-                    <v-avatar class="mr-2" size="18" color="red lighten-3"></v-avatar>
+                <div class="d-flex align-center body-2">
+                    <v-avatar class="mr-2" size="18" color="red-lighten-3"></v-avatar>
                     {{ _('Old Value') }}
                 </div>
 
                 <div class="d-flex align-center ml-4 body-2">
-                    <v-avatar class="mr-2" size="18" color="green lighten-2"></v-avatar>
+                    <v-avatar class="mr-2" size="18" color="green-lighten-2"></v-avatar>
                     {{ _('New Value') }}
                 </div>
enferno/admin/templates/admin/partials/actor_dialog.html (1)

904-915: LGTM on wrapping partials; standardize grid prop.

The new cols="12" wrappers look good and keep each partial isolated. One nit: earlier in this file Line 896 uses col="12" (singular). In Vuetify, the prop is cols. Please align for consistency.

Apply:

-<v-col col="12">
+<v-col cols="12">
enferno/admin/templates/admin/partials/incident_dialog.html (1)

133-140: Switching to partials is good; align grid prop usage.

These includes are clean and consistent. Similar to actor dialog, Line 123 above still uses col="12". Use cols="12" for consistency.

-<v-col col="12">
+<v-col cols="12">
enferno/static/js/mixins/global-mixin.js (1)

3-6: Prefer global component registration over mixin‐level
No calls to app.component('confirm-dialog', …) or app.mixin(globalMixin) were found. Instead of injecting ConfirmDialog and Toast via globalMixin in every createApp call, register them once at bootstrap (e.g. in main.js):

import { createApp } from 'vue'
import App from './App.vue'
import ConfirmDialog from '@/components/ConfirmDialog.vue'
import Toast from '@/components/Toast.vue'

const app = createApp(App)
app.component('confirm-dialog', ConfirmDialog)
app.component('toast', Toast)
app.mount('#app')

Remove them from globalMixin to ensure they’re always available without needing to apply the mixin.

enferno/static/js/mixins/form-builder-mixin.js (2)

10-12: Safer width class handling

Coerce width to a non-empty string; fall back to 'w-100' if absent.

-    fieldClass(field) {
-      return [field?.ui_config?.width || 'w-100', 'px-3 py-2']
-    },
+    fieldClass(field) {
+      const width = (typeof field?.ui_config?.width === 'string' && field.ui_config.width.trim())
+        ? field.ui_config.width.trim()
+        : 'w-100';
+      return [width, 'px-3 py-2'];
+    },

18-22: Sort should tolerate missing sort_order and avoid in-place mutation

Provide a numeric fallback and sort a shallow copy to avoid unexpected reactivity glitches.

-    sortDynamicFields() {
-      this.formBuilder.dynamicFields = this.formBuilder.dynamicFields.sort((a, b) => {
-        return a.sort_order - b.sort_order; // normal sort
-      });
-    },
+    sortDynamicFields() {
+      const hi = Number.MAX_SAFE_INTEGER;
+      this.formBuilder.dynamicFields = [...this.formBuilder.dynamicFields].sort(
+        (a, b) => (a?.sort_order ?? hi) - (b?.sort_order ?? hi)
+      );
+    },
enferno/admin/models/core_fields.py (2)

6-172: Align field_type and options with the DynamicField contract

Types like long_text, multi_select, single_select, html_block must be recognized by DynamicField. Also, “status” options here may diverge from statuses configured elsewhere (e.g., DB/ACL driven lists).

  • Confirm DynamicField’s allowed types include the above.
  • Avoid dual sources of truth for Status; consider sourcing options from the canonical statuses table/API or marking this core field as “options_managed=true” to prevent drift.

175-211: Seed updates and logging

The seeder only creates missing rows and prints to stdout. Prefer idempotent upserts and structured logging.

- from enferno.admin.models.DynamicField import DynamicField
+ from enferno.admin.models.DynamicField import DynamicField
+ from enferno.utils.logging_utils import get_logger
+ logger = get_logger()
@@
-            field.save()
-            print(f"✅ Created core field: {name}")
+            field.save()
+            logger.info("Created core field: %s", name)
         else:
-            print(f"✓ Core field already exists: {name}")
+            # Optionally upsert changed attributes to keep core fields in sync
+            updated = False
+            for attr, key in (
+                ("title", "title"),
+                ("field_type", "field_type"),
+                ("ui_component", "ui_component"),
+                ("sort_order", "sort_order"),
+                ("active", "visible"),
+            ):
+                new_val = config.get(key, getattr(existing, attr))
+                if getattr(existing, attr) != new_val:
+                    setattr(existing, attr, new_val)
+                    updated = True
+            # ui_config/options reconciliation
+            desired_ui = {"html_template": config.get("html_template")} if config.get("html_template") else {}
+            if (existing.ui_config or {}) != desired_ui:
+                existing.ui_config = desired_ui
+                updated = True
+            if config.get("options") is not None and existing.options != config["options"]:
+                existing.options = config["options"]
+                updated = True
+            if updated:
+                existing.save()
+                logger.info("Updated core field: %s", name)
+            else:
+                logger.debug("Core field already up-to-date: %s", name)

I can wire this into your CLI seed flow if you want a single “seed-core-fields” command.

enferno/admin/models/Bulletin.py (1)

23-23: Avoid potential circular import by localizing DynamicField import

If DynamicField imports Bulletin (directly/indirectly), the top-level import can create a cycle. Import where needed.

-from enferno.admin.models.DynamicField import DynamicField
+# Move this import into to_dict to avoid import cycles:
+# from enferno.admin.models.DynamicField import DynamicField

Add inside to_dict before use:

from enferno.admin.models.DynamicField import DynamicField  # localized import

Please confirm DynamicField doesn’t import Bulletin to avoid cycles; if it does, localize the import as suggested.

enferno/static/js/components/SelectFieldTypeDialog.js (3)

71-74: Normalize options default to an array (avoid nulls for non-selects)

Downstream code often expects options to be iterable. Prefer [] over null for non-select field types.

-          options: (field_type === 'single_select' || field_type === 'multi_select') ? [{ id: 1, label: this.translations.optionN_(1), value: 'option_1', hidden: false }] : null,
+          options: (field_type === 'single_select' || field_type === 'multi_select')
+            ? [{ label: this.translations.optionN_(1), value: 'option_1' }]
+            : [],

96-98: Add :key to v-for for stable rendering

Vue warns without a key; also improves diffing and performance.

-            <v-col v-for="({ label, field_type, ui_component, icon }) in componentTypes" cols="12" md="4">
+            <v-col
+              v-for="({ label, field_type, ui_component, icon }, idx) in componentTypes"
+              :key="`${field_type}-${ui_component}-${idx}`"
+              cols="12"
+              md="4">

84-84: Tweak log message to reflect action

You’re not saving here; you’re constructing and emitting.

-        console.error('Error saving field:', err);
+        console.error('Error creating field:', err);
enferno/static/js/diff-tool.js (2)

133-138: Wrap header cells in a row

<thead> should contain a <tr>. Minor HTML validity/semantics fix.

-        return `<table class="text-left w-100" style="table-layout: fixed; border-collapse: collapse;">
-                    <thead>
-                        <th class="border-b pa-1">${window?.translations?.field_ ?? 'Field'}</th>
-                        <th class="border-b pa-1">${window?.translations?.previous_ ?? 'Previous'}</th>
-                        <th class="border-b pa-1">${window?.translations?.updated_ ?? 'Updated'}</th>
-                    </thead>
+        return `<table class="text-left w-100" style="table-layout: fixed; border-collapse: collapse;">
+                    <thead>
+                      <tr>
+                        <th class="border-b pa-1">${window?.translations?.field_ ?? 'Field'}</th>
+                        <th class="border-b pa-1">${window?.translations?.previous_ ?? 'Previous'}</th>
+                        <th class="border-b pa-1">${window?.translations?.updated_ ?? 'Updated'}</th>
+                      </tr>
+                    </thead>

145-148: Expose idKey/options through getAndRenderDiff

Forward options (e.g., custom idKey) to getDiff and keep labels positioning intact.

-    getAndRenderDiff(obj1 = {}, obj2 = {}, labels = {}) {
-        const diff = DiffTool.getDiff(obj1, obj2);
+    getAndRenderDiff(obj1 = {}, obj2 = {}, labels = {}, options = {}) {
+        const diff = DiffTool.getDiff(obj1, obj2, options);
         return DiffTool.renderDiff(diff, labels);
     }
enferno/commands.py (2)

369-377: Exception handling: avoid blind catches and log with tracebacks

Catching bare Exception obscures actionable errors, and logger.error drops tracebacks. Prefer narrower exceptions where possible and use logger.exception(...).

-    except Exception as e:
-        logger.error(f"Error during initial cleanup: {str(e)}")
+    except Exception:
+        logger.exception("Error during initial cleanup")
...
-            except Exception as e:
-                logger.error(f"Error creating field {field_data['name']}: {str(e)}")
+            except Exception:
+                logger.exception("Error creating field %s", field_data["name"])
...
-        except Exception as e:
-            logger.error(f"Error testing bulletin: {str(e)}")
+        except Exception:
+            logger.exception("Error testing bulletin")
...
-    except Exception as e:
-        logger.error(f"Error in test_dynamic_fields: {str(e)}")
+    except Exception:
+        logger.exception("Error in test_dynamic_fields")
...
-            except Exception as e:
-                logger.error(f"Error cleaning up field {field.name}: {str(e)}")
+            except Exception:
+                logger.exception("Error cleaning up field %s", field.name)
...
-    except Exception as e:
-        logger.error(f"Error in cleanup: {str(e)}")
+    except Exception:
+        logger.exception("Error in cleanup")

Also applies to: 503-509, 534-540, 552-557, 577-580, 586-589


565-569: Delete all test bulletins, not just the first

Cleanup should remove every “Test Bulletin”, not only the first match.

-        test_bulletin = Bulletin.query.filter_by(title="Test Bulletin").first()
-        if test_bulletin:
-            test_bulletin.delete()
-            db.session.commit()
+        test_bulletins = Bulletin.query.filter_by(title="Test Bulletin").all()
+        for b in test_bulletins:
+            b.delete()
+        db.session.commit()
enferno/static/js/common/config.js (2)

95-117: Make focus more resilient after scrolling to the first invalid field

If the invalid element isn’t focusable (wrapper divs), focus will silently fail. Prefer focussing a child input if present.

   element.scrollIntoView({
       behavior: 'smooth',
       block: 'center',
   })
-
-  if (element.focus) {
-      setTimeout(() => element.focus(), 300) // give time for scroll
-  }
+  // Focus the element or a focusable child after scrolling
+  const focusTarget =
+    (typeof element.focus === 'function' && element) ||
+    element.querySelector?.('input, textarea, select, [tabindex]');
+  if (focusTarget && typeof focusTarget.focus === 'function') {
+    setTimeout(() => focusTarget.focus(), 300);
+  }

713-722: slugify: non‑Latin labels can yield empty values; add a safe fallback

For Arabic or other non‑Latin titles, the current regex may strip everything and leave an empty value, which later becomes an invalid key/value. Add a fallback.

 function slugify(text) {
-    return text
+    const out = text
         .toString()
         .toLowerCase()
         .trim()
         .replace(/\s+/g, '_')       // Replace spaces with underscores
         .replace(/[^\w\-]+/g, '')   // Remove non-word characters
-        .replace(/\_\_+/g, '_');    // Replace multiple underscores
+        .replace(/\_\_+/g, '_');    // Replace multiple underscores
+    // Fallback if result is blank (e.g., only non-Latin chars)
+    return out || 'value_' + Math.random().toString(36).slice(2, 8);
 }

If you prefer deterministic output, we can pass an incrementing counter or use transliteration.

enferno/static/js/components/FieldListItem.js (2)

57-58: Defensive access to theme; avoid runtime errors if $vuetify.theme is undefined

Use optional chaining to prevent crashes in non-themed contexts.

- readonly: true,
- bgColor: !this.$vuetify.theme.global.current.dark && field.core ? 'core-field-accent' : undefined
+ readonly: true,
+ bgColor: !(this.$vuetify?.theme?.global?.current?.dark) && field.core ? 'core-field-accent' : undefined

105-108: Dropdown option slug may become empty for non‑Latin labels; add fallback

Same concern as slugify(). Ensure option.value is never blank.

 if (!originalOption?.value) {
-  option.value = slugify(nextValue);
+  const s = slugify(nextValue);
+  option.value = s || `option_${option.id ?? index + 1}`;
 }
enferno/admin/templates/admin/bulletin-fields.html (3)

131-140: Avoid recomputing diffs on every render

hasChanges calls computeChanges() on each re-render, which scans the whole list. Cache the diff in data and recompute only when dynamicFields or edited inputs change.

 computed: {
-  hasChanges() {
-    const changes = this.computeChanges();
-    return Boolean(changes.create.length || changes.update.length || changes.delete.length);
-  },
+  hasChanges() {
+    const c = this.changes?.diff || { create: [], update: [], delete: [] };
+    return Boolean(c.create.length || c.update.length || c.delete.length);
+  },
 }

294-296: Update detection via JSON.stringify is brittle

JSON order and transient props can cause false positives. Use a stable, field-level deep-compare on a whitelist of properties (title, active, sort_order, ui_config, schema_config, validation_config, options).

- if (JSON.stringify(field) !== JSON.stringify(orig)) {
+ const pick = (o) => ({
+   title: o.title, active: o.active, sort_order: o.sort_order,
+   ui_config: o.ui_config, schema_config: o.schema_config,
+   validation_config: o.validation_config, options: o.options
+ });
+ if (JSON.stringify(pick(field)) !== JSON.stringify(pick(orig))) {

448-472: Soft delete vs. “delete” diff consistency

deleteField only flips active=false and keeps the item in dynamicFields, so computeChanges classifies it as an update, not a delete. If you want the red “Deleted” row in the review table, also remove the item from dynamicFields; otherwise, adjust labels to reflect “Disable”.

 onAccept: () => {
   const dynamicField = this.formBuilder.dynamicFields.find(d => d.id === field.id)
-  dynamicField.active = false;
+  dynamicField.active = false;
+  // Optional: to show as "delete" in the diff table
+  // this.formBuilder.dynamicFields = this.formBuilder.dynamicFields.filter(d => d.id !== field.id)
enferno/admin/templates/admin/jsapi.jinja2 (2)

537-542: Fix typo in translations

“Seperated” should be “Separated”.

-{"en": "Seperated", "tr": "{{ _('Seperated') }}"},
+{"en": "Separated", "tr": "{{ _('Separated') }}"},

11-486: Guard against duplicate keys in the translations object

There are repeated keys (e.g., number_, title_, tags_). While JS keeps the last one, this can hide mistakes. Consider deduping to avoid accidental overrides.

enferno/admin/views.py (4)

19-19: Remove unsafe text() import (not used after safer sort fix)

Drop text from the import list to avoid accidental use in user-influenced SQL.

-from sqlalchemy import desc, or_, asc, text, select, func
+from sqlalchemy import desc, or_, asc, select, func

3989-3990: Comment mismatch with value

S3_URL_EXPIRY = 3600 is 1 hour, not 2. Update the comment.

-S3_URL_EXPIRY = 3600  # 2 hours
+S3_URL_EXPIRY = 3600  # 1 hour

6908-6988: Prefer logger.exception over bare exception catches and generic logs

Several broad except Exception blocks log with logger.error; switch to logger.exception to keep stack traces in logs without exposing them to clients.


6670-6670: Remove unused variable

entity_type_filter is assigned but not used until the fix above; lint warns (F841).

enferno/static/css/app.css (2)

842-851: Add fallbacks for CSS variables to avoid no-op styles when tokens are undefined.

If any of the --v-* tokens aren’t provided by the theme at runtime, these helpers resolve to invalid and get dropped. Add sensible fallbacks.

-.overflow-unset { overflow: var(--v-overflow-unset); }
-.z-1 { z-index: var(--v-z-1); }
-.z-100 { z-index: var(--v-z-100); }
-.left-auto { left: var(--v-left-auto) !important; }
-.h-fit { height: var(--v-h-fit); }
-.pointer-events-none { pointer-events: var(--v-pointer-events-none) !important; }
-.pointer-events-auto { pointer-events: var(--v-pointer-events-auto) !important; }
-.rounded-16 { border-radius: var(--v-rounded-16) !important; }
-.rounded-12 { border-radius: var(--v-rounded-12) !important; }
-.rounded-10 { border-radius: var(--v-rounded-10) !important; }
+.overflow-unset { overflow: var(--v-overflow-unset, visible); }
+.z-1 { z-index: var(--v-z-1, 1); }
+.z-100 { z-index: var(--v-z-100, 100); }
+.left-auto { left: var(--v-left-auto, auto) !important; }
+.h-fit { height: var(--v-h-fit, fit-content); }
+.pointer-events-none { pointer-events: var(--v-pointer-events-none, none) !important; }
+.pointer-events-auto { pointer-events: var(--v-pointer-events-auto, auto) !important; }
+.rounded-16 { border-radius: var(--v-rounded-16, 16px) !important; }
+.rounded-12 { border-radius: var(--v-rounded-12, 12px) !important; }
+.rounded-10 { border-radius: var(--v-rounded-10, 10px) !important; }

876-915: Use theme colors for DnD indicators; hard-coded “dodgerblue” breaks theming.

Replace with rgba(var(--v-theme-primary), …) for light/dark parity. Consider 3–4px thickness for better visibility.

-    background: dodgerblue;
+    background: rgba(var(--v-theme-primary), 0.85);
enferno/admin/models/DynamicField.py (8)

341-350: Normalize entity model imports and avoid mixed import styles.

Import Actor/Bulletin/Incident in one place to reduce cycle surprises.

-    def get_entity_model(self):
-        """Get the model class for this field's entity type"""
-        from enferno.admin.models import Bulletin, Actor
-
-        models = {"bulletin": Bulletin, "actor": Actor, "incident": Incident}
+    def get_entity_model(self):
+        """Get the model class for this field's entity type"""
+        from enferno.admin.models import Bulletin, Actor, Incident
+        models = {"bulletin": Bulletin, "actor": Actor, "incident": Incident}

70-78: PR objective says 8 types (boolean, float, json) but model defines 6 (+ html_block).

Add missing types and mappings or adjust docs/CLI accordingly.

-    NUMBER = "number"  # Numeric input
+    NUMBER = "number"  # Numeric input
+    FLOAT = "float"
+    BOOLEAN = "boolean"
+    JSON = "json"
-        DATETIME: "timestamp with time zone",  # Date and time
+        DATETIME: "timestamp with time zone",  # Date and time
+        FLOAT: "double precision",
+        BOOLEAN: "boolean",
+        JSON: "jsonb",
-        DATETIME: DateTime(timezone=True),  # Date and time
+        DATETIME: DateTime(timezone=True),  # Date and time
+        FLOAT: Float,
+        BOOLEAN: Boolean,
+        JSON: JSONB,

Also extend COMPONENT_MAP/UI to cover BOOLEAN (e.g., "checkbox") as needed.


331-341: TEXT max_length validation: accept int-like strings but surface precise errors; extend to NUMBER/FLOAT ranges when provided.

Currently only TEXT max_length is validated; consider min/max for numeric types.


279-279: Log with stack traces on failures.

Use logger.exception to capture trace; already reflected in earlier diff.


352-360: get_dynamic_columns returns only active fields; confirm core=True exclusion is desired.

If core dynamic fields should also be exposed, drop the core filter here or document the behavior.


363-379: Return None for missing values to preserve type semantics.

Empty string for NUMBER/FLOAT/BOOLEAN/JSON may confuse API consumers; prefer None except for MULTI_SELECT (empty list).

-        if value is None:
-            if field_type == DynamicField.MULTI_SELECT:
-                return []
-            return ""
+        if value is None:
+            return [] if field_type == DynamicField.MULTI_SELECT else None

1-11: Remove unused imports (ForeignKey) and annotate class-level maps with ClassVar.

Keeps linters quiet and avoids accidental instrumentation.

-from sqlalchemy import (
+from sqlalchemy import (
     Column,
     String,
     Text,
     Integer,
     DateTime,
     Boolean,
     Float,
-    ARRAY as SQLArray,
-    ForeignKey,
+    ARRAY as SQLArray,
 )
+from typing import ClassVar
-    COMPONENT_MAP = {
+    COMPONENT_MAP: ClassVar[dict] = {
@@
-    TYPE_MAP = {
+    TYPE_MAP: ClassVar[dict] = {
@@
-    _column_types = {
+    _column_types: ClassVar[dict] = {

282-304: Rollback on create_column failure mirrors drop_column; ensure symmetrical transaction handling.

You added rollback in drop_column; create_column should rollback on failure too (covered in earlier diff). Also consider explicit commit on success.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

Cache: Disabled due to data retention organization setting

Knowledge Base: Disabled due to data retention organization setting

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d307633 and 30905a3.

📒 Files selected for processing (32)
  • enferno/admin/models/Bulletin.py (3 hunks)
  • enferno/admin/models/DynamicField.py (1 hunks)
  • enferno/admin/models/__init__.py (1 hunks)
  • enferno/admin/models/core_fields.py (1 hunks)
  • enferno/admin/templates/admin/bulletin-fields.html (1 hunks)
  • enferno/admin/templates/admin/bulletins.html (6 hunks)
  • enferno/admin/templates/admin/jsapi.jinja2 (2 hunks)
  • enferno/admin/templates/admin/partials/actor_dialog.html (1 hunks)
  • enferno/admin/templates/admin/partials/bulletin_dialog.html (1 hunks)
  • enferno/admin/templates/admin/partials/edit_related_actors.html (1 hunks)
  • enferno/admin/templates/admin/partials/edit_related_bulletins.html (1 hunks)
  • enferno/admin/templates/admin/partials/edit_related_incidents.html (1 hunks)
  • enferno/admin/templates/admin/partials/incident_dialog.html (1 hunks)
  • enferno/admin/templates/admin/system-administration.html (1 hunks)
  • enferno/admin/views.py (8 hunks)
  • enferno/app.py (1 hunks)
  • enferno/commands.py (4 hunks)
  • enferno/static/css/app.css (2 hunks)
  • enferno/static/js/common/config.js (5 hunks)
  • enferno/static/js/components/ConfirmDialog.js (1 hunks)
  • enferno/static/js/components/EditableTable.js (0 hunks)
  • enferno/static/js/components/FieldListItem.js (1 hunks)
  • enferno/static/js/components/FieldRenderer.js (1 hunks)
  • enferno/static/js/components/GeoMap.js (3 hunks)
  • enferno/static/js/components/SelectFieldTypeDialog.js (1 hunks)
  • enferno/static/js/components/Toast.js (1 hunks)
  • enferno/static/js/diff-tool.js (3 hunks)
  • enferno/static/js/mixins/form-builder-mixin.js (1 hunks)
  • enferno/static/js/mixins/global-mixin.js (2 hunks)
  • enferno/templates/layout.html (2 hunks)
  • enferno/templates/setup_wizard.html (0 hunks)
  • enferno/utils/base.py (3 hunks)
💤 Files with no reviewable changes (2)
  • enferno/static/js/components/EditableTable.js
  • enferno/templates/setup_wizard.html
🧰 Additional context used
🪛 Ruff (0.12.2)
enferno/admin/models/DynamicField.py

90-98: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


101-108: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


111-118: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


169-169: Avoid specifying long messages outside the exception class

(TRY003)


172-172: Avoid specifying long messages outside the exception class

(TRY003)


186-186: Avoid specifying long messages outside the exception class

(TRY003)


192-192: Avoid specifying long messages outside the exception class

(TRY003)


198-200: Avoid specifying long messages outside the exception class

(TRY003)


208-208: Avoid specifying long messages outside the exception class

(TRY003)


229-229: Abstract raise to an inner function

(TRY301)


229-229: Avoid specifying long messages outside the exception class

(TRY003)


276-276: Consider moving this statement to an else block

(TRY300)


279-279: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


279-279: Use explicit conversion flag

Replace with conversion flag

(RUF010)


298-298: Consider moving this statement to an else block

(TRY300)


301-301: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


301-301: Use explicit conversion flag

Replace with conversion flag

(RUF010)


337-337: Abstract raise to an inner function

(TRY301)


337-337: Avoid specifying long messages outside the exception class

(TRY003)


339-339: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


339-339: Avoid specifying long messages outside the exception class

(TRY003)


348-348: Avoid specifying long messages outside the exception class

(TRY003)

enferno/commands.py

65-65: Do not catch blind exception: Exception

(BLE001)


66-66: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


66-66: Use explicit conversion flag

Replace with conversion flag

(RUF010)


375-375: Do not catch blind exception: Exception

(BLE001)


376-376: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


376-376: Use explicit conversion flag

Replace with conversion flag

(RUF010)


503-503: Do not catch blind exception: Exception

(BLE001)


504-504: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


504-504: Use explicit conversion flag

Replace with conversion flag

(RUF010)


534-534: Do not catch blind exception: Exception

(BLE001)


535-535: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


535-535: Use explicit conversion flag

Replace with conversion flag

(RUF010)


552-552: Do not catch blind exception: Exception

(BLE001)


553-553: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


553-553: Use explicit conversion flag

Replace with conversion flag

(RUF010)


577-577: Do not catch blind exception: Exception

(BLE001)


578-578: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


578-578: Use explicit conversion flag

Replace with conversion flag

(RUF010)


586-586: Do not catch blind exception: Exception

(BLE001)


587-587: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


587-587: Use explicit conversion flag

Replace with conversion flag

(RUF010)

enferno/utils/base.py

129-129: Unused method argument: mode

(ARG002)

enferno/admin/views.py

3127-3127: Unused function argument: id

(ARG001)


3861-3861: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


4053-4053: Do not catch blind exception: Exception

(BLE001)


6670-6670: Local variable entity_type_filter is assigned to but never used

Remove assignment to unused variable entity_type_filter

(F841)


6677-6677: Do not catch blind exception: Exception

(BLE001)


6694-6694: Do not catch blind exception: Exception

(BLE001)


6778-6778: Do not catch blind exception: Exception

(BLE001)


6782-6784: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


6783-6783: Use explicit conversion flag

Replace with conversion flag

(RUF010)


6785-6785: Use explicit conversion flag

Replace with conversion flag

(RUF010)


6800-6800: Do not catch blind exception: Exception

(BLE001)


6801-6801: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


6801-6801: Use explicit conversion flag

Replace with conversion flag

(RUF010)


6902-6902: Do not catch blind exception: Exception

(BLE001)


6903-6903: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


6903-6903: Use explicit conversion flag

Replace with conversion flag

(RUF010)


6954-6954: Do not catch blind exception: Exception

(BLE001)


6955-6955: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


6955-6955: Use explicit conversion flag

Replace with conversion flag

(RUF010)


6984-6984: Do not catch blind exception: Exception

(BLE001)


6985-6985: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


6985-6985: Use explicit conversion flag

Replace with conversion flag

(RUF010)


7100-7100: Do not catch blind exception: Exception

(BLE001)


7101-7101: Use logging.exception instead of logging.error

Replace with exception

(TRY400)


7101-7101: Use explicit conversion flag

Replace with conversion flag

(RUF010)

🪛 GitHub Check: CodeQL
enferno/admin/views.py

[failure] 6636-6636: SQL query built from user-controlled sources
This SQL query depends on a user-provided value.


[warning] 6679-6679: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.


[warning] 6696-6696: Information exposure through an exception
Stack trace information flows to this location and may be exposed to an external user.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: semgrep/ci
🔇 Additional comments (27)
enferno/static/js/components/GeoMap.js (1)

62-66: Harden validationRules usage
GeoMap currently pulls a global validationRules binding that isn’t a window property—falling back on window.validationRules will always yield {}. In data(), replace it with:

validationRules: typeof validationRules !== 'undefined' ? validationRules : {},

—or explicitly export/import validationRules from enferno/static/js/common/config.js. For stricter wiring, consider Vue’s provide/inject.

enferno/admin/models/__init__.py (1)

14-14: Re-export looks good; watch for import cycles

LGTM for making DynamicField available via the package namespace. Please ensure DynamicField.py doesn’t import from enferno.admin.models (package) to avoid circular imports at app startup.

enferno/app.py (1)

243-244: Verified test_dynamic_fields is a Click command; registration is valid.

enferno/admin/templates/admin/partials/edit_related_actors.html (1)

1-1: LGTM: consistent spacing

Removing the outer margin aligns with related partials and container-managed spacing.

enferno/admin/templates/admin/system-administration.html (1)

1015-1015: Copy tweak LGTM

Title change reads better and remains translatable.

enferno/templates/layout.html (1)

130-132: Script load order looks correct.

ConfirmDialog/Toast are loaded before global mixins and app code. No action needed.

enferno/static/js/mixins/form-builder-mixin.js (1)

23-36: Verify dependencies in form-builder-mixin.js
Confirm that api, deepClone, and handleRequestError are imported or otherwise available before use in enferno/static/js/mixins/form-builder-mixin.js; if they’re provided by the mixin instance, reference them as this.api, this.deepClone, and this.handleRequestError.

enferno/admin/templates/admin/bulletins.html (5)

96-98: Role-gated “Edit Bulletin Form” button looks good

Visibility limited to Admin/Mod; straightforward link to the form builder.


384-384: Including form-builder mixin

Loaded before app init; OK.


440-440: Registering FieldRenderer

Component registration is correct, assuming the script is loaded above. Looks good.


476-476: App mixin injection

formBuilderMixin inclusion is correct and ordered after global mixins.


1791-1792: Expose FieldRenderer globally

Matches the global usage pattern elsewhere; good.

enferno/admin/models/Bulletin.py (1)

694-696: Base dict merge

Using super().to_dict() is fine; just be mindful of overlapping keys (you overwrite below). No action needed.

enferno/static/js/diff-tool.js (1)

2-3: hasDiff return-type change is safe; no callers detected

enferno/static/js/common/config.js (3)

122-144: LGTM: theme tokens

The design tokens look good and are neatly grouped for reuse across themes.


222-226: Verify Vuetify “variables” support in current version

Vuetify 3’s public API doesn’t document a top-level theme “variables” prop the same way Vuetify 2 did. If you’re on Vuetify 3, consider CSS vars via styles or a custom plugin, or confirm your theme plugin reads this key.

Would you confirm the Vuetify version in use and how these variables are consumed (plugin or built-in)? If needed, I can propose a small wrapper to expose these tokens as CSS variables.

Also applies to: 244-245


399-399: Route placeholder for bulletin-fields has an empty component

This will render a blank view unless the page mounts its own content. If SSR/template injects content for this route, ignore. Otherwise, consider lazy-loading the actual editor component or redirecting to the admin page that holds the builder.

Do you intend this route only for in-page anchors? If not, I can provide a code-split import stub.

enferno/admin/templates/admin/partials/bulletin_dialog.html (1)

173-174: Guard against unsafe dynamic keys when binding editedItem[field.name]

If a malicious or invalid name like proto, constructor, or prototype slips through, this could cause prototype pollution. The backend claims reserved-name checks; add a lightweight UI guard as defense-in-depth.

Would you confirm the server enforces a denylist (['proto','constructor','prototype']) and DB-identifier rules? If desired, I can add a tiny client-side check in FieldRenderer/FieldListItem to block these names.

enferno/static/js/components/FieldListItem.js (1)

100-107: Hard dependency on global slugify; verify load order

FieldListItem assumes a global slugify from config.js. If scripts load out-of-order, this will throw.

Confirm that common/config.js is loaded before FieldListItem.js on all admin pages using the builder. If not, I can switch to a small local slug util as a fallback: const toSlug = (s) => (window.slugify?.(s) ?? localSlug(s)).

enferno/admin/templates/admin/bulletin-fields.html (3)

13-17: Verify slot syntax and interpolation for Vuetify v3 data-table row slots

Using a dynamic slot name with #[item.type] is fine in Vue 3, but the inner text uses ${item.type}. Ensure your Vue delimiters are configured to "${", "}" at runtime, otherwise this won’t interpolate and the raw string will render.


260-274: Partial-save UX: confirm dialog state on failures

When some requests fail, you recompute diffs and then throw the first error. Confirm the dialog stays open with the recomputed table so users can retry without losing context.


328-442: Drag-and-drop implementation looks solid

Good use of Sortable to cancel DOM swaps, show directional drop lines, and reindex after move.

enferno/admin/templates/admin/jsapi.jinja2 (1)

59-61: Status keys addition looks good

added_/modified_/deleted_ will be useful for the review table and align with the new UI.

enferno/admin/views.py (1)

6596-6617: filter[field][_eq] parsing bug; support _eq and basic type coercion

The docs show [_eq] but the code checks “eq”. As written, filter[entity_type][_eq]=bulletin won’t match. Strip leading underscores from op; optionally coerce booleans.

-            parts = key[7:-1].split("][_")
+            inner = key[7:-1]
+            parts = inner.split("][")
             if len(parts) == 2:
                 field, op = parts
-                if op == "eq":
-                    if hasattr(DynamicField, field):
-                        filters.append(getattr(DynamicField, field) == value)
+                op = op.lstrip("_")
+                if op == "eq" and hasattr(DynamicField, field):
+                    col = getattr(DynamicField, field)
+                    # Optional: minimal boolean coercion
+                    if isinstance(getattr(type(col), "python_type", str), type) and getattr(col.type, "python_type", str) is bool:
+                        value = str(value).lower() in ("1","true","t","yes","y","on")
+                    filters.append(col == value)

Likely an incorrect or invalid review comment.

enferno/static/css/app.css (1)

842-851: Verified all helper selectors are in use and no dead selectors remain. No references to removed helpers (e.g. .chip-container) were found; .pointer-events-*, .rounded-*, .h-fit, .left-auto appear across components and config.

enferno/admin/models/DynamicField.py (2)

149-150: Unique constraint length may collide with long entity/field names (63-char limit).

Consider hashing in _make_index_name (see earlier support code) and apply similarly for constraint/index names if any others are added.


241-243: Remove reference to _canonical_name()—it’s undefined. If you need to normalize identifiers, implement a canonicalization helper (e.g. self.name.lower()) or use SQLAlchemy’s QuotedName for safe identifier quoting.

Likely an incorrect or invalid review comment.

apodacaduron and others added 20 commits September 19, 2025 09:42
- Introduced a new migration to create the dynamic_fields table, ensuring compatibility with the DynamicField model.
- Added core bulletin fields to maintain functionality for existing deployments after the upgrade.
- Included necessary indexes for optimized querying on entity_type, active, and core fields.
- Change multi_select/single_select to select with allow_multiple config
- Add schema_config support in generate_core_fields()
- Fixes installation errors during core field creation
apodacaduron and others added 19 commits October 31, 2025 11:24
…h array overlap operator validation and ensure numeric values are converted to strings in SQL queries.
- Upgrade brotli from 1.1.0 to 1.2.0 (fixes CVE-2025-6176 decompression DoS)
- Upgrade pypdf from 6.0.0 to 6.1.3 (fixes GHSA-vr63-x8vc-m265, GHSA-jfx9-29x2-rv3j)
- Install brotli 1.2.0 from GitHub until PyPI release is available
- Install git in base build stage to support GitHub dependency installation
- Required for brotli@v1.2.0 installation from git+https://github.com/google/brotli
- Git only present in build stage, not in final runtime image (multi-stage build)
- Applied to both production Dockerfile and test Dockerfile
- Removed default limit of 25; now returns all fields by default with a maximum cap of 1000 for safety.
- Updated API calls in form builder mixin to remove hardcoded limit parameter, allowing for more flexible data retrieval.
@tarekio tarekio merged commit cc89955 into main Nov 1, 2025
10 of 11 checks passed
@tarekio tarekio deleted the dynamic-content-fresh branch November 1, 2025 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants