Просмотр исходного кода

Add Claude skills for removing models & fields

Jeremy Stretch 2 недель назад
Родитель
Сommit
3d0308a95f
2 измененных файлов с 407 добавлено и 0 удалено
  1. 213 0
      .claude/skills/remove-model-field/SKILL.md
  2. 194 0
      .claude/skills/remove-model/SKILL.md

+ 213 - 0
.claude/skills/remove-model-field/SKILL.md

@@ -0,0 +1,213 @@
+---
+name: remove-model-field
+description: Step-by-step checklist for removing a field from an existing NetBox model, covering all required touch points (model, migration, serializer, forms, filterset, table, panel/template, search, GraphQL, tests, docs). Use when the user asks to remove or delete a field or attribute from an existing model.
+---
+
+# Removing a Field from an Existing NetBox Model
+
+Removing a field touches many files. Work through the checklist below in order — remove outer consumers first (tests, docs, GraphQL, API, forms) before touching the model definition itself.
+
+## Before You Start
+
+Determine upfront:
+- **Field name** and which **model/app** owns it
+- **Field type**: scalar, FK/M2M, GenericForeignKey, or special (JSONField, etc.)
+- **All references** — run a broad grep before touching anything:
+
+```bash
+grep -r 'new_field\|related_thing' netbox/ --include='*.py' -l
+grep -r 'new_field\|related_thing' docs/ -l
+```
+
+For FK/M2M fields, also check for FilterSet `_id` companions and GraphQL lazy annotations referencing this field.
+
+**Check dependents**: if other models or code use this field (e.g. ordering, constraints, signal handlers), those references must be cleaned up too.
+
+## 1. Update Tests
+
+Update test files to remove references to the field being deleted. Specifically:
+
+- **`tests/test_filtersets.py`** — remove `test_<field>` and `test_<field>_id` methods; remove the field from `setUpTestData` test objects.
+- **`tests/test_api.py`** — remove the field from `setUpTestData`, `create_data`, and `bulk_update_data`; remove any `test_list_objects_by_<field>` methods.
+- **`tests/test_views.py`** — remove the field from `form_data`, `bulk_edit_data`, and `csv_data` in `setUpTestData`.
+- **`tests/test_models.py`** — remove any `test_clean_<field>` or constraint tests specific to this field.
+
+## 2. Update Documentation
+
+**File:** `docs/models/<app>/<modelname>.md`
+
+Remove the field's entry from the `## Fields` section. If the field had any cross-references in other doc pages, remove those too.
+
+## 3. Update GraphQL
+
+### Filter — `graphql/filters.py`
+
+Remove the filter field declaration(s) for the deleted field:
+
+```python
+# Remove lines like:
+new_field: StrFilterLookup[str] | None = strawberry_django.filter_field()
+
+# Or for FK:
+related_thing: Annotated[...] | None = strawberry_django.filter_field()
+related_thing_id: ID | None = strawberry_django.filter_field()
+```
+
+### Type — `graphql/types.py`
+
+For simple fields, `fields='__all__'` means no change is needed — the field disappears automatically once removed from the model.
+
+For FK fields with an explicit annotation, remove the annotation line:
+
+```python
+# Remove:
+related_thing: Annotated['RelatedThingType', strawberry.lazy('<app>.graphql.types')] | None
+```
+
+If the field was in an `exclude` list, remove it from the exclude list (it no longer exists to exclude).
+
+## 4. Update the API Serializer
+
+**File:** `netbox/<app>/api/serializers_/<module>.py`
+
+- **Simple field**: remove the field name from `Meta.fields` (and `brief_fields` if present).
+- **FK field**: remove the serializer field declaration and its name from `Meta.fields`:
+
+```python
+# Remove:
+related_thing = RelatedThingSerializer(nested=True, required=False, allow_null=True)
+# And remove 'related_thing' from Meta.fields
+```
+
+## 5. Update Forms
+
+There are typically up to four forms to update. Find them under `netbox/<app>/forms/`.
+
+### 5a. Filter form — `forms/filtersets.py`
+
+- Remove the field from `fieldsets`.
+- Remove the filter field declaration (e.g. `new_field = forms.CharField(...)` or the `DynamicModelMultipleChoiceField`).
+
+### 5b. Bulk edit form — `forms/bulk_edit.py`
+
+- Remove the field from `fieldsets` and `Meta.fields` (if present).
+- Remove the field declaration.
+- Remove from `nullable_fields` if listed there.
+
+### 5c. Bulk import form — `forms/bulk_import.py`
+
+- Remove from `Meta.fields`.
+- Remove any explicit field declaration.
+
+### 5d. Model form — `model_forms.py`
+
+- Remove from `fieldsets`.
+- Remove from `Meta.fields`.
+- Remove any explicit field declaration (e.g. a `DynamicModelChoiceField`).
+
+## 6. Update the FilterSet
+
+**File:** `netbox/<app>/filtersets.py`
+
+- **Simple field**: remove from `Meta.fields`.
+- **FK field**: remove both the `<field>` and `<field>_id` explicit filter declarations.
+- **`search()` method**: if the field was included in the `Q(...)` chain, remove that clause.
+- Remove any now-unused imports (e.g. the related model import if it was only used by this filter).
+
+## 7. Update the Table
+
+**File:** `netbox/<app>/tables/<module>.py`
+
+- Remove the column declaration (e.g. `related_thing = tables.Column(linkify=True)`).
+- Remove the field from `Meta.fields`.
+- Remove from `default_columns` if listed there.
+
+## 8. Update the Detail View Panel
+
+**File:** `netbox/<app>/ui/panels.py`
+
+Find the panel class for the model and remove the attribute declaration:
+
+```python
+# Remove:
+new_field = attrs.TextAttr('new_field')
+related_thing = attrs.RelatedObjectAttr('related_thing', linkify=True)
+```
+
+If the model uses a legacy HTML template (`netbox/templates/<app>/`) rather than a declarative panel, remove the corresponding `<tr>` row from that template instead.
+
+## 9. Update the SearchIndex
+
+**File:** `netbox/<app>/search.py`
+
+If the field was indexed for global search, remove it from the `fields` tuple:
+
+```python
+# Remove:
+('new_field', 300),
+```
+
+## 10. Remove the Field from the Model
+
+**File:** `netbox/<app>/models/<module>.py`
+
+1. Delete the field declaration.
+2. If the field was in `clone_fields`, remove it from that tuple.
+3. If `clean()` had validation logic specific to this field, remove those clauses. If `clean()` becomes empty, remove the override entirely.
+4. For FK fields: remove the `related_name` on the target model is automatic (Django handles it). If the FK was the only reason a related model was imported, remove that import too.
+5. For GenericForeignKey fields: if this was the only GFK, also remove the `object_type` ContentType FK and `object_id` integer field, and remove the `models.Index(fields=('object_type', 'object_id'))` from `Meta`.
+
+## 11. Generate the Migration
+
+**Do NOT write migrations manually.** Tell the user to run:
+
+```bash
+cd netbox/
+python manage.py makemigrations <app> -n remove_<field>_from_<model> --no-header
+```
+
+Set `DEVELOPER = True` in `configuration.py` if the command is blocked.
+
+Review the generated migration — it should contain only a `RemoveField` operation (plus any index removal for GFK fields). Apply with:
+
+```bash
+python manage.py migrate
+```
+
+## Summary Checklist
+
+| # | File(s) | Action |
+|---|---|---|
+| 1 | `tests/test_*.py` | Remove field from test data, filter tests, API tests, view tests |
+| 2 | `docs/models/<app>/<model>.md` | Remove field from `## Fields` section |
+| 3 | `graphql/filters.py`, `types.py` | Remove filter field; remove FK annotation if explicit |
+| 4 | `api/serializers_/<module>.py` | Remove from `Meta.fields`; remove FK serializer field |
+| 5a | `forms/filtersets.py` | Remove from `fieldsets`; remove filter field declaration |
+| 5b | `forms/bulk_edit.py` | Remove from `fieldsets`, `Meta.fields`, `nullable_fields` |
+| 5c | `forms/bulk_import.py` | Remove from `Meta.fields` and field declaration |
+| 5d | `forms/model_forms.py` | Remove from `fieldsets`, `Meta.fields`, and field declaration |
+| 6 | `filtersets.py` | Remove from `Meta.fields`; remove FK + FK_id pair; update `search()` |
+| 7 | `tables/<module>.py` | Remove column declaration and from `Meta.fields`, `default_columns` |
+| 8 | `<app>/ui/panels.py` | Remove attr declaration from panel class |
+| 9 | `search.py` | Remove from SearchIndex `fields` tuple |
+| 10 | `models/<module>.py` | Remove field; clean up `clone_fields`, `clean()`, imports |
+| 11 | (user runs) | `makemigrations <app> -n remove_<field>_from_<model> --no-header` then `migrate` |
+
+## Common Gotchas
+
+- **Work outside-in** — remove tests, docs, GraphQL, and API references before touching the model, to avoid import errors during the process.
+- **FK fields leave no `_id` companion in serializers** — the modern pattern uses a single `field = Serializer(nested=True)`. Grep for the field name and the serializer class name.
+- **FilterSets have both `<field>` and `<field>_id`** — both must be removed; they are explicit declarations, not auto-generated.
+- **`clone_fields`** must be updated if the field was listed there.
+- **`search()` in filtersets** — if the field was in the `Q(...)` chain of the `search()` method, that clause must be removed to avoid a `FieldError` at runtime.
+- **`brief_fields` in serializers** — remove explicitly if the field was listed.
+- **`makemigrations` must be run**, not written manually. If blocked, set `DEVELOPER = True` in `configuration.py`.
+- **No `ruff format`** on existing files — use `ruff check` only.
+
+## References
+
+- Panel attrs reference: `netbox/netbox/ui/attrs.py`
+- Panel classes: `netbox/<app>/ui/panels.py`
+- Base filterset classes: `netbox/netbox/filtersets.py`
+- `add-model-field` skill: `.claude/skills/add-model-field/SKILL.md` (reverse of this skill)
+- Contributing guide: `docs/development/extending-models.md`

+ 194 - 0
.claude/skills/remove-model/SKILL.md

@@ -0,0 +1,194 @@
+---
+name: remove-model
+description: Step-by-step guide for removing an existing model from NetBox, covering all required touch points in safe deletion order (tests, docs, nav, search, GraphQL, API, views, URLs, forms, filterset, table, choices, model, migration). Use when the user asks to remove, delete, or deprecate a model or object type from NetBox.
+---
+
+# Removing a Model from NetBox
+
+Removing a model requires undoing ~13 components. Work in the order below — remove consumers before providers to avoid import errors during the process. Deleting a model is **irreversible once migrated**; confirm with the user before running `makemigrations`.
+
+## 0. Before You Start
+
+Identify:
+- **Model name** and **app** — e.g. `MyModel` in `dcim`
+- **All references** — run a broad grep before touching anything:
+
+```bash
+grep -r 'MyModel\|mymodel\|my-model\|my_model' netbox/ --include='*.py' -l
+grep -r 'MyModel\|mymodel\|my-model\|my_model' docs/ -l
+grep -r 'mymodel\|my-model' netbox/netbox/navigation/ --include='*.py'
+```
+
+Check for:
+- Other models with ForeignKey / M2M pointing to this model (they need updating or their own removal first)
+- Generic relations via `FeatureQuery` or `ContentType` that reference this model
+- Any plugin or external code documented as depending on this model
+
+**Do not proceed if other retained models have non-nullable FKs to this model** — those FK fields must be removed or made nullable first.
+
+## 1. Remove Tests
+
+Delete test methods or entire test classes that exist solely for this model. If the test file contains only this model's tests, delete the file; otherwise remove just the relevant class(es).
+
+Files to check:
+- `netbox/<app>/tests/test_api.py`
+- `netbox/<app>/tests/test_views.py`
+- `netbox/<app>/tests/test_filtersets.py`
+- `netbox/<app>/tests/test_models.py`
+- `netbox/<app>/tests/test_forms.py`
+- `netbox/<app>/tests/test_tables.py`
+- Any app-specific test modules (e.g. `test_cablepaths.py`)
+
+## 2. Remove Documentation
+
+1. Delete `docs/models/<app>/<modelname>.md`.
+2. Remove the `mkdocs.yml` entry under the relevant `nav:` group.
+3. Remove the entry from `docs/development/models.md` (the "Models Index" list).
+
+## 3. Remove Navigation Menu Entry
+
+**File:** `netbox/netbox/navigation/menu.py`
+
+Remove the `get_model_item('<app>', 'mymodel', ...)` line from the relevant `MenuGroup`.
+
+## 4. Remove from Search Index
+
+**File:** `netbox/<app>/search.py`
+
+Delete the `@register_search` class for the model. If the file becomes empty (no other indexes), delete the file itself.
+
+## 5. Remove GraphQL
+
+Remove in this order (schema depends on types, types depend on filters):
+
+1. **`netbox/<app>/graphql/schema.py`** — remove the `my_model` and `my_model_list` fields from the app's `Query` type.
+2. **`netbox/<app>/graphql/types.py`** — remove the `MyModelType` class and its `__all__` entry.
+3. **`netbox/<app>/graphql/filters.py`** — remove the `MyModelFilter` class and its `__all__` entry.
+
+If any remaining type in `types.py` has a lazy annotation referencing `MyModelType`, remove that annotation too.
+
+## 6. Remove REST API
+
+1. **`netbox/<app>/api/urls.py`** — remove the `router.register('my-models', ...)` line.
+2. **`netbox/<app>/api/views.py`** — remove the `MyModelViewSet` class.
+3. **`netbox/<app>/api/serializers_/<module>.py`** — remove the serializer class. If this was the only serializer in the module, delete the file and remove its `from .<module> import *` line from `serializers_/__init__.py`.
+
+Also check other serializers that reference this model (e.g. `MyModelSerializer(nested=True)` on related serializers) and remove those fields too.
+
+## 7. Remove URL Routes
+
+**File:** `netbox/<app>/urls.py`
+
+Remove the two `path(...)` entries that call `get_model_urls('<app>', 'mymodel', ...)`.
+
+## 8. Remove Views
+
+**File:** `netbox/<app>/views.py`
+
+Remove all view classes decorated with `@register_model_view(MyModel, ...)`. There are typically seven:
+
+- `MyModelListView`
+- `MyModelView`
+- `MyModelEditView`
+- `MyModelDeleteView`
+- `MyModelBulkImportView`
+- `MyModelBulkEditView`
+- `MyModelBulkDeleteView`
+- `MyModelBulkRenameView` (if present)
+
+Also remove the panel class from `netbox/<app>/ui/panels.py` and any `layout` references using it.
+
+If there is a model-specific HTML template (`netbox/templates/<app>/mymodel.html` or similar), delete it.
+
+## 9. Remove Table
+
+**File:** `netbox/<app>/tables/<module>.py`
+
+Remove the `MyModelTable` class. If it is the sole table in the module, delete the file and clean up the `__init__.py` re-export.
+
+**File:** `netbox/<app>/tables/__init__.py`
+
+Remove the corresponding `from .<module> import *` or named import.
+
+## 10. Remove Forms
+
+Remove in dependency order (bulk forms depend on the model form):
+
+1. **`netbox/<app>/forms/bulk_import.py`** — remove `MyModelImportForm`.
+2. **`netbox/<app>/forms/bulk_edit.py`** — remove `MyModelBulkEditForm`.
+3. **`netbox/<app>/forms/filtersets.py`** — remove `MyModelFilterForm`.
+4. **`netbox/<app>/forms/model_forms.py`** — remove `MyModelForm`.
+5. **`netbox/<app>/forms/__init__.py`** — remove all re-exports of the deleted form classes.
+
+## 11. Remove FilterSet
+
+**File:** `netbox/<app>/filtersets.py`
+
+Remove the `MyModelFilterSet` class. Also remove any imports of `MyModel` or related models that were only used by this filterset.
+
+## 12. Remove Choices
+
+**File:** `netbox/<app>/choices.py`
+
+Remove any `ChoiceSet` subclasses that were defined exclusively for this model (e.g. `MyModelStatusChoices`). Leave choices that are shared with other models.
+
+## 13. Remove the Model
+
+**File:** `netbox/<app>/models/<module>.py` (or `models.py`)
+
+1. Delete the `MyModel` class.
+2. Remove `'MyModel'` from `__all__` in the module.
+3. Remove the import line in `netbox/<app>/models/__init__.py` if this was the last model in the submodule (or remove just the `MyModel` name from a `from .<module> import ...` line).
+4. Remove any now-unused imports in the model file itself.
+
+## 14. Generate the Migration
+
+**Do NOT write migrations manually.** Tell the user to run:
+
+```bash
+cd netbox/
+python manage.py makemigrations <app> -n remove_mymodel --no-header
+```
+
+Set `DEVELOPER = True` in `configuration.py` if the command is blocked.
+
+Review the generated migration before applying — it should only contain a `DeleteModel` operation (plus any `RemoveField` operations for FKs on other models if Django detected them). Apply with:
+
+```bash
+python manage.py migrate
+```
+
+## Common Gotchas
+
+- **Remove consumers before providers** — tests, docs, GraphQL schema, API viewset, URL routes, and views all reference the model; remove them before removing the model itself to avoid import errors.
+- **FK cleanup** — Django will detect FKs pointing at the deleted model and auto-add `RemoveField` operations to the migration. Verify the migration is correct before running it.
+- **ContentType cleanup** — after migrating, `ContentType` rows for the old model linger in the database. They are harmless but can be cleaned up with `python manage.py remove_stale_contenttypes`.
+- **`__all__` entries** — grep all `__init__.py` files for the model name after removing the class; dangling re-exports cause `ImportError` at startup.
+- **Serializer references** — other serializers may have a nested `MyModelSerializer(nested=True)` field. Search for the serializer class name as well as the model name.
+- **`manage.py` lives in `netbox/`**, not the repo root.
+- **No `ruff format`** on existing files — use `ruff check` only.
+
+## Summary Checklist
+
+| # | File(s) | Action |
+|---|---|---|
+| 1 | `tests/test_*.py` | Remove test classes for this model |
+| 2 | `docs/models/<app>/<model>.md`, `mkdocs.yml`, `docs/development/models.md` | Delete doc page; remove nav entries |
+| 3 | `netbox/netbox/navigation/menu.py` | Remove `get_model_item(...)` line |
+| 4 | `<app>/search.py` | Remove `SearchIndex` class |
+| 5 | `<app>/graphql/schema.py`, `types.py`, `filters.py` | Remove query fields, type, filter |
+| 6 | `<app>/api/urls.py`, `views.py`, `serializers_/<module>.py` | Remove router entry, viewset, serializer |
+| 7 | `<app>/urls.py` | Remove `get_model_urls(...)` paths |
+| 8 | `<app>/views.py`, `<app>/ui/panels.py` | Remove all view classes and panel |
+| 9 | `<app>/tables/<module>.py`, `tables/__init__.py` | Remove table class and re-export |
+| 10 | `<app>/forms/*.py`, `forms/__init__.py` | Remove all four form classes and re-exports |
+| 11 | `<app>/filtersets.py` | Remove `FilterSet` class |
+| 12 | `<app>/choices.py` | Remove model-specific `ChoiceSet` subclasses |
+| 13 | `<app>/models/<module>.py`, `models/__init__.py` | Remove model class and `__all__` entry |
+| 14 | (user runs) | `makemigrations <app> -n remove_mymodel --no-header` then `migrate` |
+
+## References
+
+- Model base classes: `netbox/netbox/models/__init__.py`
+- Navigation menu: `netbox/netbox/navigation/menu.py`
+- `add-model` skill: `.claude/skills/add-model/SKILL.md` (reverse of this skill)