Dynamic Fields System for Flexible Entity Customization#100
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughA 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
Sequence DiagramsequenceDiagram
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120+ minutes Areas requiring extra attention:
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
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. Comment |
There was a problem hiding this comment.
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) coordinatesUsing 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 diffsUser-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('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + + 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 errorsPrefer 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 clientsMirror 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 dialogDropping 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 consistencySame 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 warningsfield.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 onlyrejectPromise 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 errorsUse 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 300msThe 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 namesPrefer 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 toapp.component('confirm-dialog', …)orapp.mixin(globalMixin)were found. Instead of injectingConfirmDialogandToastviaglobalMixinin everycreateAppcall, register them once at bootstrap (e.g. inmain.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
globalMixinto 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 handlingCoerce 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 mutationProvide 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 contractTypes 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 loggingThe 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 importIf 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 DynamicFieldAdd inside to_dict before use:
from enferno.admin.models.DynamicField import DynamicField # localized importPlease 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: Normalizeoptionsdefault to an array (avoid nulls for non-selects)Downstream code often expects
optionsto be iterable. Prefer[]overnullfor 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:keyto v-for for stable renderingVue 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 actionYou’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 getAndRenderDiffForward
options(e.g., custom idKey) togetDiffand 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 tracebacksCatching bare
Exceptionobscures actionable errors, andlogger.errordrops tracebacks. Prefer narrower exceptions where possible and uselogger.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 firstCleanup 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 fieldIf 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 fallbackFor 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 undefinedUse 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 fallbackSame 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 renderhasChanges 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 brittleJSON 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 consistencydeleteField 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 objectThere 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 valueS3_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 logsSeveral 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 variableentity_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.
📒 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: HardenvalidationRulesusage
GeoMap currently pulls a globalvalidationRulesbinding that isn’t awindowproperty—falling back onwindow.validationRuleswill always yield{}. Indata(), replace it with:validationRules: typeof validationRules !== 'undefined' ? validationRules : {},—or explicitly export/import
validationRulesfromenferno/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 cyclesLGTM 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: Verifiedtest_dynamic_fieldsis a Click command; registration is valid.enferno/admin/templates/admin/partials/edit_related_actors.html (1)
1-1: LGTM: consistent spacingRemoving the outer margin aligns with related partials and container-managed spacing.
enferno/admin/templates/admin/system-administration.html (1)
1015-1015: Copy tweak LGTMTitle 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 thatapi,deepClone, andhandleRequestErrorare imported or otherwise available before use inenferno/static/js/mixins/form-builder-mixin.js; if they’re provided by the mixin instance, reference them asthis.api,this.deepClone, andthis.handleRequestError.enferno/admin/templates/admin/bulletins.html (5)
96-98: Role-gated “Edit Bulletin Form” button looks goodVisibility limited to Admin/Mod; straightforward link to the form builder.
384-384: Including form-builder mixinLoaded before app init; OK.
440-440: Registering FieldRendererComponent registration is correct, assuming the script is loaded above. Looks good.
476-476: App mixin injectionformBuilderMixin inclusion is correct and ordered after global mixins.
1791-1792: Expose FieldRenderer globallyMatches the global usage pattern elsewhere; good.
enferno/admin/models/Bulletin.py (1)
694-696: Base dict mergeUsing 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 detectedenferno/static/js/common/config.js (3)
122-144: LGTM: theme tokensThe design tokens look good and are neatly grouped for reuse across themes.
222-226: Verify Vuetify “variables” support in current versionVuetify 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 componentThis 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 orderFieldListItem 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 slotsUsing 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 failuresWhen 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 solidGood 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 goodadded_/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 coercionThe 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-autoappear 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’sQuotedNamefor safe identifier quoting.Likely an incorrect or invalid review comment.
- 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.
…to dynamic-content-fresh
…yanat into dynamic-content-fresh
- 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
…to dynamic-content-fresh
This reverts commit e7c7829.
…yanat into dynamic-content-fresh
…h array overlap operator validation and ensure numeric values are converted to strings in SQL queries.
…to dynamic-content-fresh
- 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
This reverts commit 4531af1.
This reverts commit 6610dac.
- 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.
…to dynamic-content-fresh
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
Architecture Changes
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
flask test-dynamic-fieldsFiles Changed
enferno/admin/models/DynamicField.py- New dynamic field modelenferno/utils/base.py- Enhanced BaseMixin with dynamic field supportenferno/admin/models/Bulletin.py- Minimal integration changesenferno/commands.py- Added CLI commands for testingenferno/admin/models/__init__.py- Model registrationUsage Example
Dynamic fields are automatically included in JSON serialization without any additional code changes.
Summary by CodeRabbit
Release Notes
New Features
UI Enhancements