Upgrade from VUEDA v2
This guide moves a project from server v2.3.3 and client v2.0.4 to the v3 beta. Each step names a change that needs action in your project and links the page that explains the current behavior.
Upgrade the server and the client together. A v3 client cannot read the metadata that a v2 server sends, and a v2 client cannot read the v3 metadata. Neither side reports a metadata version, so a mismatch produces broken screens instead of a clear error.
Before You Begin
- Take a full database backup.
- Archive the django-simple-history tables. The upgrade deletes them, and VUEDA does not copy their rows into the v3 history. Archive v2 History lists the tables.
- Read Assess v2 Security Exposure. Several v2 defects need a review of existing data or accounts.
- Plan a test upgrade on a copy of the production database before you upgrade production.
Archive v2 History
Server v2 recorded history in django-simple-history tables. VUEDA's nine workflow models wrote vueda_workflow_historical* tables. Your models wrote a Historical* table when they subclassed VuedaHistoryBaseModel, SimpleHistoryModelMixin, or AbstractVUEDAUserWithHistory, or when they declared HistoricalRecords.
Server v3 records history with django-pghistory event models. The vueda_workflow migration 0009 deletes the nine workflow history tables. When you run makemigrations, Django generates DeleteModel operations for your own Historical* tables. After the upgrade, history lists start empty, and an object's workflow state history starts at its next state change.
Export every Historical* table before you apply those migrations. A user history table from AbstractVUEDAUserWithHistory holds password hashes. Protect that export the same way you protect the user table. #511 tracks a conversion of v2 history rows into v3 events.
Upgrade the Server
Update Python, Django, and Dependencies
Server v3 requires Python 3.12 to 3.14, Django 6.1, and Django REST framework 3.18 or later. It installs from the public PyPI index as vueda. Remove any private index and source binding for vueda from your pyproject.toml. The beta is a prerelease, so opt in when you add it:
uv add --prerelease allow vuedaThese dependencies changed:
django-simple-historyis no longer a dependency. Keep it as a direct dependency of your project while any of your migrations still importsimple_history. Alternatively, edit those migrations to drop the import and theHistoricalChangesentry in eachbases=tuple.drf-flex-fieldsbecamedrf-flex-fields2. Rename theREST_FLEX_FIELDSsetting toREST_FLEX_FIELDS2, and changerest_flex_fieldsimports to submodules ofrest_flex_fields2. The package root ofrest_flex_fields2exports nothing.django-pghistory,django-pgtrigger, andchannelsare new dependencies.dj-rest-authmoved to version 7 anddjango-anymailto version 15.- Redis support is the
redisextra. Installvueda[redis]when your cache is Redis.
Update Settings
get_defaults() reads and sets these keys differently:
CACHE_URLreplacesREDIS_URL. Server v2 built a Redis cache fromREDIS_URLwith the key prefixvueda-. Server v3 buildsCACHES["default"]from a requiredCACHE_URLand sets no prefix. Add?key_prefix=vueda-to a RedisCACHE_URLto keep existing sessions. Without it, the deploy signs every user out once. Configure the Cache and Sessions describes the backends.FRONTEND_DOMAINis a full origin. Server v2 puthttps://in front of the value. Server v3 uses the value as written. Changeapp.example.comtohttps://app.example.com. Without the scheme, welcome and password-reset emails carry broken links.NO_REPLY_EMAILis required.get_defaults()reads it at startup.- History middleware. Remove
simple_history.middleware.HistoryRequestMiddlewarefrom anyMIDDLEWARElist or ASGI stack that you build yourself.get_defaults()addsvueda.history.middleware.VuedaHistoryMiddlewareafterAuthenticationMiddleware. A hand-builtMIDDLEWAREmust add it at that position. The checksvueda_history.W001andW002report a missing or misplaced middleware. - Logging. Remove references to the
ignore_validation_warningsfilter and to thevueda.core.logging_filtersmodule. Neither exists in v3, and a reference fails at startup. - CORS. If you replace
CORS_ALLOW_HEADERS, addacknowledge-warnings. If you replaceCORS_EXPOSE_HEADERS, addretry-after. - Throttle rates. The
anon,user, anddj_rest_authrates are gone, and several auth rates are new. Override single keys ofDEFAULT_THROTTLE_RATESinstead of replacing the dictionary. Rate-Limited Auth Endpoints lists the rates and the proxy settings that identify a client address. - Anymail webhooks. When
EMAIL_BACKENDis the Mailgun backend, server v2 stored the webhook secret under a key that Anymail never read. Server v3 setsANYMAIL_WEBHOOK_SECRETin that case, so Anymail now checks basic authentication on every webhook. Adduser:password@credentials that match the secret to each Mailgun webhook URL. Without them, Anymail rejects tracking events, and queue items stay inawaiting.get_defaults()sets no webhook secret for other email backends, so setANYMAIL_WEBHOOK_SECRETyourself when you use one. - Empty choices. VUEDA no longer reads
EMPTY_CHOICE_LABELorEMPTY_CHOICE_VALUE. Remove them from your settings.
Update Your Code
- Base model.
VuedaBaseModelandVuedaHistoryBaseModelare nowVuedaModel.AbstractVUEDAUserWithHistoryis nowAbstractVUEDAUser. No old name remains as an alias. - History classes.
SimpleHistoryModelMixin,VuedaHistoryViewSet,VuedaHistorySerializer,SimpleHistoryViewSetMixin,SimpleHistorySerializerMixin,HistoricalRecordField, andcreate_historical_recordare removed. Remove them from your models, serializers, and viewsets. AVuedaModelsubclass records history without a mixin, and everyVuedaViewSetfor a tracked model serveshistory-list. Code that readinstance.historymust query the model's event model. Find the Event Model for a Tracked Model shows how. - History policy. Every
VuedaModelsubclass records history unless it opts out. Composite primary key models must opt out. History never recordspassword. History and Workflow describes the policy and its options. Runmakemigrations, review the generated event models and triggers, and exclude noisy fields such aslast_login. - Writes outside a request. Wrap writes in management commands and scripts in
audited_action(..., kind="command")so thataudited_actiongroups their history into one action. - Workflow opt-in.
HasWorkflowModelMixin,HasWorkflowSerializerMixin,HasWorkflowViewMixin,HasWorkflowViewSetMixin, andHasWorkflowFilterSetMixinare removed. Declareclass Vueda.Workflow: enabled = Trueon the model instead. VUEDA then adds the workflow fields, theworkflow_statefilter, and the permission overlay. History and Workflow describes the options. - Default manager.
FormattedNameManageris the default manager of every VUEDA model. A customobjectsmanager must subclass it or useFormattedNameManager.from_queryset(...). On a model that declaresformatted_name_lookup_expression, every other manager must also annotateformatted_name, or opt out withused_without_formatted_name = True. Opting a Manager Out of the Check describes the opt-out. For a model that declaresformatted_name_lookup_expression,Model.objects.values()without arguments now includesformatted_name. - Model info hook.
get_expandable_fields()is removed, and VUEDA silently ignores a v2 override. Overrideget_expand_model_info(self, expands)instead. Correct an Expand Descriptor shows the hook. - Validation warnings.
VuedaValidationError(..., is_warning=True)is removed. Return warnings fromVuedaSerializer.get_warnings(), or from the viewset'sget_warnings_for_objectandget_warnings. The client confirms them through a409response. Require Confirmation Before a Write covers each hook. - Column totals.
column_totalsis a mapping from the client column name to an ORM path. Rewrite each v2 list as a mapping.get_column_infotakes(self, queryset, requested_column_totals=()). The server computes totals only for the names that the request sends inct. Expose Aggregates inlistResponses describes the contract. - Extra query parameters.
get_extra_allowed_fieldsis an instance method. An override must extend the result ofsuper(). Otherwise, the server rejectsct. - Search through many-valued relations. De-duplicated search builds its subquery from the viewset's
get_search_queryset()when the viewset defines one. Otherwise, it uses the model's default manager. Asearch_fieldsentry that names an annotation fromget_queryset()needs aget_search_queryset()that adds it. The checkvueda_info.E014reports the problem. Search Through a Many-Valued Relation explains the hook. - Nested writes. A nested write to a forward foreign key, one-to-one, or many-to-many relation now runs the related model's viewset permissions. Register a canonical viewset for each related model that a nested write changes, and grant create or update on it. Set Up Permissions for Related Rows lists the steps.
- Permission name mapping.
PERMISSION_NAMES_MAPPINGaccepts only two values: the default mapping, and the mapping to Django's names,{"create": "add", "update": "change", "read": "view", "list": "view"}. The checkvueda_core.E018reports any other value. Map Django and VUEDA Permission Names covers both values. - File and image fields. Model
FileFieldandImageFieldcolumns serialize as{name, url}. Declare a stock DRFFileFieldorImageFieldon the serializer to keep a plain URL string for one field. - Unhandled exceptions. VUEDA's exception handler now rolls back the request transaction when it answers an unhandled exception with
500. Server v2 committed the writes that a custom view made before it raised. A custom view that must keep such a write must make it outside the request transaction.
Run the Migrations
Object states carried over from v2 keep their current states when a current v3-generated workflow migration runs, even though they have no v3 events. How Object States Are Updated describes the rules and how to refresh older v3-generated files.
WARNING
Upgrade VUEDA before running makeworkflowmigrations on the upgraded database. Older v3 versions reset object states with no events even when the command reports no workflow changes. Before applying an older v3-generated workflow migration, refresh its embedded functions with updateworkflowmigrations, review the changes, and commit them. A package upgrade alone does not change those copies. Migrations generated with --import-instead receive the fix through the package upgrade. The fix does not restore states that were already reset.
WARNING
Do not reverse a workflow migration that v2 generated. Its reverse code deletes the object states of every workflow, including workflows that the migration did not create. updateworkflowmigrations does not rewrite these files.
Test a full replay of your migrations on an empty database before you deploy. A v2-generated workflow migration looks up the Historical* workflow models, which vueda_workflow 0009 deletes. When your dependency graph lets 0009 run first, the replay fails. #510 tracks a supported path for these files.
- Run
python manage.py updategroupmigrationsbefore you apply v2-generated group migrations in another environment. The v2 replay code deleted a group, with its memberships, when the group lost its last permission. The command rewrites that code. Updating Existing Group Migrations describes it. - Run
makemigrationsand review the result. Expect event models and triggers for each tracked model, andDeleteModeloperations for eachHistorical*table. - Review the migration plan with
python manage.py migrate --plan. On an empty database, check that every workflow migration that v2 generated runs beforevueda_workflow0009. On the database that you upgrade, check that any v3-generated workflow migration has the current functions described above.updateworkflowmigrationsstill skips v2-generated files and reports them as failures; review its output for each file. - Apply the migrations.
- Run
python manage.py check --database default. Ifvueda_workflow.W002reports models with rows that have no workflow state, runpython manage.py backfillworkflowstates. Enabling Workflow on a Model with Existing Rows explains the check. - Run
python manage.py remove_stale_contenttypes. The deletedHistorical*models leave content types behind, and the permission pickers keep offering their permissions until you remove those content types.
Fix What the System Checks Report
Server v2 registered no system checks. Server v3 registers checks in vueda_core, vueda_info, vueda_history, and vueda_workflow. An error stops runserver and migrate. Run python manage.py check and fix each finding. The checks that v2 projects most often hit are:
vueda_core.E018: an unsupportedPERMISSION_NAMES_MAPPING.vueda_info.E006: a viewsetorderingorordering_fieldsentry that names no field.vueda_core.E019: a manager that does not annotateformatted_nameon a model that declaresformatted_name_lookup_expression.vueda_info.E010: aquerysetorget_queryset()ordering that disagrees with the declared ordering, that orders by fields alone with no declared ordering, or that calls.reverse()on a sortable list. It also reports a grouped list queryset that makes Django dropMeta.ordering, and aget_queryset()orquerysetquery that Django rejects.vueda_info.E013: a column total path that crosses a reverse or many-to-many relation, or that ends on a field that cannot be summed.vueda_info.E014: a search annotation that the search queryset lacks.vueda_info.E015: a default manager that orders by fields alone. Move that ordering toMeta.ordering. It also reports a default manager whose query Django rejects, such as a misspelled field,F()name, or transform.
Review Authorization Changes
Several v2 permission paths admitted requests that v3 refuses. Before you deploy, grant the permissions below so that users keep their access:
HEADrequests need the permission that the matchingGETneeds. Server v2 required none. Required Codename per Request lists the rules.- State permission rows no longer bypass model permissions. In v2, a viewset that used
HasWorkflowViewMixinskipped its model permission checks whenever the model's workflow had anyStatePermissionrow. That covered list, create, and other actions that do not fetch one object. In v3, a state grant admits a request only when it matches the user's group and the requested permission. It then admits only rows in the granted states, and create always needs the model permission. For each group that used such a viewset, decide which access it should keep. Grant a model permission only where the group should reach rows in every state. Model-Scope Deferral and Later Decisions explains the rules. - Conflicting state rules resolve to deny. In v2, the outcome depended on row order.
- A workflow needs
WorkflowPermissionrows. In v2, anyStatePermissionrow on a workflow skipped the workflow permission check. A workflow that hasStatePermissionrows but noWorkflowPermissionrows now refuses every transition, for superusers too. - Transitions need object
read. Transition discovery and execution require thereadpermission on each target object.read_workflowalone no longer lets a user list or run an object's transitions. Which Gate Each Workflow Endpoint Applies lists every endpoint. - Field choices for serializer-only fields require
readon the serializer's model. A related field also requiresliston the related model. Server v2 served these choices to any caller, including anonymous ones. - Filter choices come only from rows that the user may list.
Update API Consumers
The VUEDA client handles these changes. Update any other client or integration that calls the API:
- Ordering terms and filter names that cross a relation use dots:
?o=-customer.name, notcustomer__name. An invalid ordering term answers400. Public Filter Names describes the names. - An unrecognized query parameter answers
400onlistandretrieve. Server v2 answered500on a filteredlistand ignored the parameter elsewhere.retrieveaccepts onlye,f, andom. fandomshape only the response on writes. APOSTorPUTmust include every required field. Sparse Fields and Expand on Writes states the rule.POST,PUT, andPATCHcheck the names infandethe same way thatretrievedoes. An unknownfname, or an unknown or unpermittedename, answers400, and the write saves nothing. Server v2 saved the write and left the unknownfname out of the response.- An
epath deeper than 4 levels answers400. - Bulk activate and deactivate answer
400for a key inpksthat names no row, as bulk delete does. A row that the user may not change counts as missing. Server v2 skipped such keys and counted them in its success message. - Object responses include
available_actionsonly whenfnames it. model_orderingis an object withdefaultandfields.model_filteringreports dotted names. Filter choices have no empty option, and their values are strings.- Invalid
fandenames come back as plain strings in the error list. - A nested write matches child primary keys only among the parent's own children. A v2 client that moved a child to another parent by sending its key under the new parent now creates a new row. To move a child, update its foreign key through the child's own endpoint.
model_filteringreportsAllValuesChoiceFieldandAllValuesMultipleChoiceFieldas thetype_filterof value-derived filters, where v2 reportedChoiceFieldandMultipleChoiceField.vueda.user/_allauth/is no longer mounted. Recovery codes are atvueda.user/2fa/recovery-codes/.- The who-is response carries
auth_pending_flowin place ofrecently_logged_in. - VUEDA routes
vueda.user/forgot-password/andvueda.user/reset-password/. Remove your own routes for those views. Both answer204on success, and forgot-password answers204for every well-formed address. Errors that are not about one field carrydetailin place of v2'sresultandmessage. Field errors stay keyed by field name. - History responses group events by user action. Workflow
valid_transitionsentries are{code, name}objects. - The workflow object-state and execute-transition responses carry
object_state_revision, a string, in place of the integercurrent_history_id. Object and who-is responses no longer carrycurrent_history_id. History-tracked objects carryobject_revisioninstead. - Workflow state history moved from
object-history/<app_label>/<model>/<object_id>/(URL nameget-object-history) tovueda.history/workflow-state-history/<app_label>/<model>/<object_id>/(URL nameworkflow-state-history).vueda.history.urlscarries its own prefix, so include it at""in your routes. Wire Up Workflow URLs shows the include. - Debug-only pages moved under the app prefixes:
vueda.user/dev-login/,vueda.user/dev-logout/,vueda.user/permissions/, andvueda.workflow/. The URL nameslocal-loginandlocal-logoutare nowdev-loginanddev-logout.
Upgrade the Client
Update Dependencies and the Build
The client installs from the public npm registry as @arrai-innovations/vueda. Point the @arrai-innovations scope at the public registry, and name the beta tag when you add it:
pnpm add @arrai-innovations/vueda@beta- Remove the peers that v3 no longer uses:
primevue,@primeuix/themes,quill,@floating-ui/vue,@arrai-innovations/pro-duotone-svg-icons, and@arrai-innovations/vue-fontawesome. - Install
@arrai-innovations/vue-sonner2.0.11 or later. Importtoastfrom it, not from upstreamvue-sonner. - Raise
@arrai-innovations/reactive-helpersto 25 and@vueuse/coreto 14. Both have breaking changes of their own. - Use Node 22 or later. The package does not declare this requirement, so a package manager does not warn about an older Node.
- Install Tailwind CSS 4 and
@tailwindcss/vite. - Spread
vuedaViteConfig()into your Vite config, and pass yoursrcdirectory asextraAliases["@"]. Without that alias, VUEDA does not find convention-named action views. View Component Resolution Order describes the lookup.
Client Plugin Prerequisites lists the setup that each package needs.
Replace PrimeVue Setup
VUEDA's controls are its own, built on Reka UI. Remove PrimeVue setup that existed only for VUEDA: app.use(PrimeVue), the preset, setPrimeVuePreset, warningsPrimeVueConfig, and ToastService.
- Import
@vueda/theme/vueda-tailwind/base.cssaftertailwindcssin your stylesheet. Dark mode follows a.darkclass on an ancestor. - Mount
<Sonner />once. Without it, VUEDA's toasts do not render.requireGroupsaccepts the v2 toast options, but passing the PrimeVue severitysecondaryorcontrastnow throws. - Register icons. The icon registry starts empty. Call
installFontAwesomeFreeIcons()and install the Font Awesome Free packages, or register your own icon components withsetIcons. - Remove
pt,mergeSections, andmergePropsfromwidgetPropsandfieldProps. - Rewrite slot overrides that render PrimeVue components or read PrimeVue slot props such as
severityandverb.
Move the Page Title and Sticky Bars to the Layout
Views register their title with usePageTitle(), and the root layout renders it. Call usePageTitle() in your root component. Wrap <RouterView> in <StickyStackProvider> and put <PageTitle /> in its top slot. Remove per-page <PageTitle title="..."> components. Place the Page Title and Page Actions shows the layout.
Update Imports
The @vueda/components/ and @vueda/fields/ folders no longer exist. Components moved into family folders:
| Components | Folder |
|---|---|
ActionForm, AuthForm, AuthorizingForm, ModelActionForm | @vueda/views/ |
FormModel, FieldRenderer | @vueda/form/form-model/ |
FieldSetMany, FieldSetRange, FieldSetSingularStackedInline, FieldSetStackedInline, FieldSetStackedInlineRow, FieldSetTabularInline | @vueda/form/field-set/ |
FilterForm, FilterGroup | @vueda/form/filter/ |
ObjectsGrid and its cell and header components | @vueda/objects-grid/ |
PageTitle, StickyBar | @vueda/shell/page-title/, @vueda/shell/sticky/ |
NavigationItem, LinkModelView | @vueda/navigation/item/, @vueda/navigation/link-model-view/ |
ClickToCopyText, DateTimeDisplay, DateRangeDisplay, ErrorDisplay, LoadingSpinnerBlock, LoadingSpinnerInline | Folders under @vueda/display/ |
EmptyComponent, LazyRender | @vueda/support/ |
These components are removed:
| Removed | Use instead |
|---|---|
DetailedView | DetailView |
FilterComponent | FilterMenu, FilterChip, and FilterFieldForm |
MobileSortComponent | SortControl with SortGroup and SortChip |
PaginationComponent | PaginationFooter |
FormChores, FormFeedback, FormHelpText, FormHiddenFeedback | The field shell components and FormMessage |
NavigationDisplay | useNavigation with NavigationItem, or the sidebar components |
ViewWorkFlowTransition | ViewExecuteTransition |
Field<Type> components such as FieldString and FieldDate | FormField with fieldProps |
storeCollapseNav | SidebarProvider with v-model:open |
useWarnings, useSearchableSelect, useObjectsWorkflowTransitions | None |
The widgets that wrapped PrimeVue controls have new names:
| Field type | v3 widget |
|---|---|
CharField | WidgetTextInput |
TextField | WidgetTextTextarea |
| Number fields | WidgetNumberInput |
DateField, DateTimeField | WidgetDateField |
TimeField | WidgetTimeField |
Date ranges, TimeRangeField | WidgetDateRangeField, WidgetTimeRangeField |
| Relations | WidgetCombobox |
| Choices | WidgetSelectDropdown or WidgetRadioGroup |
BooleanField | WidgetToggle |
JSONField | WidgetJson |
Rename widgetComponents entries, mergeDefaultFieldMappings registrations, and direct imports. Recheck every custom widgetProps, because the PrimeVue props such as showTime no longer apply. A fieldComponents or widgetComponents string that names an unregistered component now shows an error in place of the field.
Update Theme Overrides
setTheme, patchTheme, getTheme, mergeTheme, and useTheme keep their v2 names, and a theme entry still maps a component's slots to classes. Use overrideTheme for project changes. Unlike patchTheme, it does not depend on module load order. How the layers interact explains the order.
ViewListdropsvariantand its*Classprops, andObjectsGriddrops its*Classprops.DetailViewhas noouterClass. Style the region around its form through thebodyslot of its theme entry. Move each one to athemeOverrideprop or to anoverrideThemeentry.ViewCreateandDetailViewkeepvariantandformModelVariant, which select a model config variant.- Theme keys and slot names changed with the components. Retarget each v2 patch at the current key.
mergeThemethrows when it receives fewer than two layers. Write a variable-length merge asmergeTheme({}, ...layers).themeOverrideaccepts only an object.
Update Routes
makeCRUDRoutesredirects. PassactionRedirect, pointing at a route thatrequireModelInfodoes not guard. PassgroupsRedirectwhenevergroupscontains a group.makeCRUDRoutesthrows without them.routeActionstakes effect. The v2 guard read a key that model config never set, sorouteActionshad no effect in v2. In v3, it limits which action routes open. Audit everyrouteActionsyou set. It uses canonical action names, so listretrieve, notread. Limit Which Routes Open shows the setting.- Transitions route by code. Link to a transition by its
code. Rename any transition code that equals acrudComponentskey. - Multi-object routes repeat
pk. Write?pk=4&pk=7, not?pk=4,7. Bookmarks in the old form select one key named4,7.getCRUDForTothrows when the forwardedqueryalready containspk. - Guard renames.
waitForInitialisingis nowwaitForCurrentUser, andrequireInitializedis nowrequireCurrentUser. - Guard placement. The CRUD checks run from a global
beforeEach, and the route records have nobeforeEnter. Move code that extendedbeforeEnterinto your own guard. - Auth routes. Define routes named
sign-in,reauthenticate,2fa, andwelcome. The auth composables navigate to those names. - Custom action views. VUEDA keeps a custom view mounted across app, model, and
pkchanges when the resolved component stays the same. Watch those props instead of reading them once at setup.
Update Model Config and Metadata Reads
storeModelInfocamelCases top-level keys and expand descriptor keys:appLabel,verboseName,verboseNamePlural. Rename reads, fixtures, and seeded store data. Client Normalization lists the rules.expanddefaults to an empty list. Add each relation that a view must embed to that view'sexpand. A list view also needs the relation in the viewset'spermit_list_expands. A form that must edit a relation inline needs the relation inexpand. Without it, a create or update form shows a reverse foreign key read-only and leaves it out of the request body. When the viewset setspermit_<action>_expandsfor the form's action, the relation must be in that list too.- Model config throws when
expandnames an undeclared expand, or when a dotted field entry needs a relation thatexpandomits. - Keys for expanded subfields use dots:
employee.name, notemployee__name. This applies tofieldComponents,widgetComponents,fieldProps,widgetProps,columnComponents,sortables,filterableDetails, and slot names such asfield(employee.name). - Default field lists differ per view, and they leave out hidden fields. By default,
submitFieldsholds writable fields, and the create view shows writable fields. Choose Each View's Field Lists describes the defaults. submitFieldslimits the request body. Name every value that the form must send, including values that a custom widget sets without an input.fetchFieldssets thefparameter of a save request.sortedstarts from the server's default ordering. Passsorted: []to start unsorted.
Update Forms
- Replace
Field<Type>components withFormField. - Warnings use a confirmation step. Remove
setUsingWarningscalls. A custom create or update shell built onuseObjectFormmust renderFormConfirmDialogbound toobjectForm.confirmation. Without it, a save that returns warnings resolves as cancelled. - A custom CRUD adapter reports field errors by throwing a
ServerFeedbackErrorsubclass. A submission error that the form does not recognize becomesobjectForm.state.error. - Saves with files send a multipart body with a manifest. A custom DRF view that receives those saves must list
NestedMultipartMixinbefore its DRF base class. Multipart Saves describes the format. - File and image widgets read the
urlof a{name, url}value. WidgetDurationtakes a duration string. With thesecondsprop, it takes a number of seconds.- Read-only fields render formatted values. The
widget(<name>)defaultslot receivesrawValuefor code that needs the stored value. - On update, read, and detail views, a view's own form props, such as
fieldPropsandwidgetProps, now win over the same keys in model configformProps. Client v2 let model config win on those views. Check each key that both set. FormModel'sfieldsslot binds the merged field props map asallFieldProps. Render each field in a customfieldslayout withFormModelField, which receives the per-field slots that you pass to the form. Lay Out the Fields Yourself shows the layout.- A custom
redirectToonActionFormthat stays on the form after a success must return or resolvefalse. The form treats any other result as navigation away and keeps its buttons disabled. What an Action Form Does on Pre-Flight, Submit, and Cancel describes the contract. - Save buttons read "Create" and "Update". Update tests that find "Submit".
- The
escapeHtmlexport is removed. Send markup unencoded when it should render. - Saved inline rows offer a delete checkbox whenever the inline is writable. The parent update deletes omitted children and does not check a child destroy permission. To stop users with parent
updatefrom removing children, validate the child list on the server.
Update List Views
FilterGrouptakes the array of active filters as itsv-model.filterables,filterableDetails, andvalidFilterablesare required props.ViewListdropsfilterFormsValues,allowShowAllPages, andalwaysShowAllPages. Itsfilter-changeevent is nowfiltered, and it emits once with a live ref.ViewListopens at 25 rows unless the user saved another size, and sends that size asps. The server'sPAGE_SIZEno longer applies. PassdefaultPageSizeto start at another size, ordefaultPageSize="all"where a model usedalwaysShowAllPages. Keep page size options at or below the server'sMAX_PAGE_SIZE.ObjectsGridhas no sorting props. Sort withSortControlandSortGroup. ReplacegirdTypeslot checks withisTableLayoutandisCardLayout.ViewListwrites a sort that the user picks to the URL aso.- Columns render through typed adapters: foreign keys as links, and dates, booleans, durations, and JSON through their own adapters. Set
columnComponents: { <column>: "ColumnText" }to keep plain text. Customize List Column Rendering covers the adapters. - The filter menu offers only filter types that the client can render. Register a missing type with
mergeFilterFieldMapping. Custom Filter Types describes the mapping. - A project-registered
listadapter must passrepeatedParamstomakeSearchParamsString. A custom bulk delete adapter must acceptformDataand treat a dry-run200as success. - The client ignores saved sorts and filters that use
__names, so users see the defaults once.
Update Auth and Store Code
storeUser.pendingFlowis nowauthPendingFlow, a string. ReplacerecentlyLoggedInwithloggedIn && !authPendingFlow. Replaceinit()withwaitForCurrentUser(pinia).initializedandinitializingPromiseare removed.AuthFormsends a user with a pending reauthentication to thereauthenticateroute as soon as the form opens. SetrequireRecentAuthtofalseon anAuthFormwhose action does not need a recent sign-in.- Users with a two-factor device reauthenticate with a code. Mount
ViewReauthenticateat thereauthenticateroute. Build a Re-Authentication View covers it. setUsingVuedaWorkFlowandgetUsingVuedaWorkFloware nowsetUsingVuedaWorkflowandgetUsingVuedaWorkflow.storeWorkflowdropsfetchObjectState,fetchObjectTransitions,fetchModelStates, andinitializeObjectTransitions, and theobjectStates,objectTransitions, andmodelStatescaches. Read an object's state from itsworkflow_state_codeandworkflow_state_namefields, and its transitions fromvalid_transitions. A transition response carries the new state. Nothing replaces the list of a model's states.- The custom URL key
workflowObjectHistoryis nowhistoryWorkflowStateHistory, and its value is a full template with:app,:model, and:pk. Update eachsetCustomUrlcall for that key. ViewHistoryListfield names changed fromhistory_*torecorded_at,actor,kind,label,model,relation,type,field,old, andnew. Its slots follow the new names. ViewHistoryList lists them.
Assess v2 Security Exposure
v3 fixes these v2 defects. Each one may need a review of data or accounts that v2 created:
- Public signup. Server v2 mounted every allauth headless route, including
vueda.user/_allauth/browser/v1/auth/signup. Anyone could create an account and get a signed-in session. Review accounts that nobody on your side created. Server v3 does not mount the allauth routes. Do not mount allauth's signup route yourself: VUEDA's account adapter leaves signup open, so a mounted signup route accepts new accounts. - Nested writes across parents. Server v2 looked up nested child rows by primary key across the whole child table. A user who could update one parent could move another parent's child row into it and change it. Review child rows whose parent changed.
- Nested writes to related models skipped the related model's permissions.
- State permission bypass. On a viewset that used
HasWorkflowViewMixin, anyStatePermissionrow on the model's workflow let every authenticated user pass the model permission checks. That covered list, create, and other actions that do not fetch one object. The workflow endpoints skipped their model check in the same case. Transitions also ignored objectread. - Password hashes in history.
AbstractVUEDAUserWithHistoryrecorded password hashes, and the v2 history endpoints andhistoryexpand could return them to anyone who could read the user. - Forgot-password enumeration. Server v2 answered "Email not found" for unknown addresses.
HEADrequests skipped read permission, so the status code revealed which primary keys exist.- Bulk delete, activate, and deactivate checked only the model permission. They skipped the row-level and object permission checks that a single-object request makes. A user could therefore delete, activate, or deactivate rows that their row-level rules kept them from changing one at a time. Review deletions and
is_activechanges on models that defineRowLevelPermissionsor use object permissions. - Choices. Serializer-only field choices had no permission check, and filter choices came from every row.
- Two-factor changes accepted a password-only reauthentication.
- Two-factor device deletion. A bulk delete of a user's own devices compared the number of devices deleted with the number of keys sent. A repeated or unknown key could turn off two-factor authentication while the user still had devices. It could also leave two-factor authentication on with no device left. Only the user's own recent session could send such a request. Review accounts whose two-factor state does not match their devices.
- Authenticator app removal. All of a user's two-factor devices share one secret. Removing an authenticator app while another device remained left that secret unchanged. The removed app still produced codes that confirmed sign-in. v3 gives the secret a new value when a user adds or removes an authenticator app. The upgrade does not change existing secrets. Ask users who removed an app under v2 to add an authenticator app again, which replaces the secret. The same defect broke an app added after an email or SMS device. Its codes never worked, and it keeps failing until the user removes it and sets it up again.
- Rate limits. Code sends and password changes had no limit. Forgot-password had only a one-minute cooldown for addresses with an account.
- Anymail webhooks. On the Mailgun backend, the tracking endpoints of providers other than Mailgun accepted unauthenticated events.
- Group migrations deleted a group, with its user memberships, when it lost its last permission.
- Client caches kept the previous user's metadata, choices, and workflow data after a sign-out without a page reload. The server still checked every request.
Verify the Upgrade
python manage.py checkreports no errors.- A fresh database replays every migration.
- A user from each group that uses the application can sign in, open its lists and forms, and save.
- Users with two-factor devices can sign in and reauthenticate.
- Workflow transitions appear and run for the groups that should have them.
- History lists show a change made after the upgrade.
- The browser console shows no unregistered-component, unmapped-filter, or route warnings.