Flagsmith / Flagsmith/flagsmith
Project export endpoint
- Dominant language
- Python
- Stars
- 6.6k
- Forks
- 567
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 121
Description
## Scope
- New module: `api/features/project_import_export/`
- New model: `ProjectExport`
- Inherits `abstract_base_auditable_model_factory()` for audit log entries.
- `history_record_class_path`, `related_object_type` (new `RelatedObjectType.PROJECT_EXPORT`)
- `project` FK
- `created_by` FK to `FFAdminUser` (nullable)
- `created_by_api_key` FK to `MasterAPIKey` (nullable)
- `status` (PROCESSING / SUCCESS / FAILED)
- `data` — `TextField`. Max size 10MB (vs 1MB for env-level).
- `created_at`
- `include_identity_overrides` bool
- `include_tags` bool
- Audit log messages:
- `get_create_log_message`: "Project export requested"
- `get_update_log_message`: "Project export completed" or "Project export failed"
- `get_audit_log_author` returns `self.created_by`
- `_get_project` returns `self.project`
- New task processor task: `export_project(project_export_id: int)`
- Query project data and serialize using the export serializers above.
- Wrap in `ProjectExportDataSerializer`.
- Store in `data` field, set status = SUCCESS.
- New views:
- `POST /projects/{project_id}/project-exports/` — create export (accepts optional toggle booleans).
- `GET /projects/{project_id}/project-exports/` — list exports.
- `GET /projects/{project_id}/project-exports/{id}/download/` — download JSON file.
- Request/response serializers for create (with toggle fields) and list.
- Permissions: project admin.
- Recurring cleanup task: delete exports older than 14 days.
- Migration for new model.
- Tests: 100% diff coverage.
## Acceptance criteria (write as tests first)
- `POST` with toggles creates an async export, returns 202.
- `GET` list shows all exports for the project with status.
- `GET` download returns the JSON file with correct Content-Disposition header.
- Export JSON matches the schema in #7125 exactly.
- Exports without optional data omit those top-level keys entirely.
- Stale exports are cleaned up after 14 days.
- Adding a new field to an existing model serializer (e.g. a new environment setting) should automatically appear in the export without modifying export code.
## Serializer composition strategy
New fields added to models or existing serializers should automatically appear in project exports
without modifying the export code. We achieve this by subclassing existing DRF serializers and
using `exclude` instead of `fields` on Meta classes.
For each entity, create a thin export serializer that inherits from the existing serializer and
overrides only what's needed:
```python
# Self-contained sub-objects — reuse directly, new fields auto-included:
# FeatureStateValueSerializer → {type, string_value, integer_value, boolean_value}
# NestedMultivariateFeatureOptionSerializer → {type, string_value, ..., default_percentage_allocation}
# SegmentRuleSerializer → {type, rules[], conditions[]}
# ConditionSerializer → {operator, property_, value, description}
# Top-level entities — thin subclasses with exclude + FK overrides:
class TagExportSerializer(TagSerializer):
class Meta(TagSerializer.Meta):
exclude = ("id", "uuid", "project", "is_system_tag", "type")
class SegmentExportSerializer(SegmentSerializer):
class Meta(SegmentSerializer.Meta):
exclude = ("id", "uuid", "project", "feature", "version_of",
"created_at", "updated_at", "metadata")
class FeatureExportSerializer(CreateFeatureSerializer):
tags = serializers.SlugRelatedField(slug_field="label", many=True, read_only=True)
class Meta(CreateFeatureSerializer.Meta):
exclude = ("id", "uuid", "project", "created_date", "owners", "group_owners",
"is_archived")
class EnvironmentExportSerializer(EnvironmentSerializerLight):
feature_states = FeatureStateExportSerializer(many=True, read_only=True)
feature_segments = FeatureSegmentExportSerializer(many=True, read_only=True)
identity_overrides = IdentityOverrideExportSerializer(many=True, read_only=True)
class Meta(EnvironmentSerializerLight.Meta):
exclude = ("id", "uuid", "project", "api_key", "is_creating",
"webhooks_enabled", "webhook_url", "updated_at", "created_date")
class FeatureStateExportSerializer(serializers.Serializer):
feature = serializers.SlugRelatedField(slug_field="name", read_only=True)
enabled = serializers.BooleanField()
feature_state_value = FeatureStateValueSerializer()
multivariate_feature_state_values = MVStateValueExportSerializer(many=True)
class FeatureSegmentExportSerializer(serializers.Serializer):
feature = serializers.SlugRelatedField(slug_field="name", read_only=True)
segment = serializers.SlugRelatedField(slug_field="name", read_only=True)
priority = serializers.IntegerField()
feature_state = FeatureStateExportSerializer()
class ProjectExportDataSerializer(serializers.Serializer):
version = serializers.IntegerField()
exported_at = serializers.DateTimeField()
tags = TagExportSerializer(many=True, required=False)
segments = SegmentExportSerializer(many=True)
features = FeatureExportSerializer(many=True)
environments = EnvironmentExportSerializer(many=True)
```
The pattern: `exclude` on Meta so new model fields are auto-included. FK fields overridden to
`SlugRelatedField` (name-based) or dropped (implicit from nesting). Nested serializers
(`FeatureStateValueSerializer`, `SegmentRuleSerializer`, etc.) reused directly.
`multivariate_feature_option` in `MVStateValueExportSerializer` is the only custom field —
resolves the FK to an array index.
Contributor guide
Assessment
This issue has not been assessed yet.