university-creator 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. university_creator-0.1.0/.gitignore +39 -0
  2. university_creator-0.1.0/LICENSE +21 -0
  3. university_creator-0.1.0/PKG-INFO +72 -0
  4. university_creator-0.1.0/README.md +38 -0
  5. university_creator-0.1.0/pyproject.toml +57 -0
  6. university_creator-0.1.0/src/university/creator/__init__.py +15 -0
  7. university_creator-0.1.0/src/university/creator/admin.py +54 -0
  8. university_creator-0.1.0/src/university/creator/api/__init__.py +0 -0
  9. university_creator-0.1.0/src/university/creator/api/v1/__init__.py +0 -0
  10. university_creator-0.1.0/src/university/creator/api/v1/serializers.py +374 -0
  11. university_creator-0.1.0/src/university/creator/api/v1/urls.py +80 -0
  12. university_creator-0.1.0/src/university/creator/api/v1/views.py +569 -0
  13. university_creator-0.1.0/src/university/creator/apps.py +127 -0
  14. university_creator-0.1.0/src/university/creator/conf.py +71 -0
  15. university_creator-0.1.0/src/university/creator/constants.py +101 -0
  16. university_creator-0.1.0/src/university/creator/content.py +92 -0
  17. university_creator-0.1.0/src/university/creator/events.py +20 -0
  18. university_creator-0.1.0/src/university/creator/handlers.py +53 -0
  19. university_creator-0.1.0/src/university/creator/integrations.py +87 -0
  20. university_creator-0.1.0/src/university/creator/management/__init__.py +0 -0
  21. university_creator-0.1.0/src/university/creator/management/commands/__init__.py +0 -0
  22. university_creator-0.1.0/src/university/creator/management/commands/sync_creator_accounts.py +20 -0
  23. university_creator-0.1.0/src/university/creator/migrations/0001_initial.py +289 -0
  24. university_creator-0.1.0/src/university/creator/migrations/0002_participation_fee_payment.py +23 -0
  25. university_creator-0.1.0/src/university/creator/migrations/__init__.py +0 -0
  26. university_creator-0.1.0/src/university/creator/models.py +296 -0
  27. university_creator-0.1.0/src/university/creator/payables.py +66 -0
  28. university_creator-0.1.0/src/university/creator/providers.py +153 -0
  29. university_creator-0.1.0/src/university/creator/selectors.py +162 -0
  30. university_creator-0.1.0/src/university/creator/services/__init__.py +69 -0
  31. university_creator-0.1.0/src/university/creator/services/accounts.py +223 -0
  32. university_creator-0.1.0/src/university/creator/services/analytics.py +114 -0
  33. university_creator-0.1.0/src/university/creator/services/campaigns.py +495 -0
  34. university_creator-0.1.0/src/university/creator/services/profiles.py +246 -0
  35. university_creator-0.1.0/src/university/creator/services/verification.py +125 -0
  36. university_creator-0.1.0/src/university/creator/tasks.py +5 -0
  37. university_creator-0.1.0/tests/__init__.py +0 -0
  38. university_creator-0.1.0/tests/audience/__init__.py +0 -0
  39. university_creator-0.1.0/tests/audience/apps.py +28 -0
  40. university_creator-0.1.0/tests/conftest.py +22 -0
  41. university_creator-0.1.0/tests/fakes.py +20 -0
  42. university_creator-0.1.0/tests/settings.py +9 -0
  43. university_creator-0.1.0/tests/test_creator.py +536 -0
  44. university_creator-0.1.0/tests/urls.py +3 -0
@@ -0,0 +1,39 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .eggs/
6
+ build/
7
+ dist/
8
+ .venv/
9
+ venv/
10
+ .nox/
11
+ .tox/
12
+ .pytest_cache/
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .coverage
16
+ coverage.xml
17
+ htmlcov/
18
+
19
+ # Django
20
+ *.sqlite3
21
+ # Local user uploads (never the university.media source package).
22
+ /media/
23
+ /private-media/
24
+ example_project/media/
25
+ example_project/private-media/
26
+ staticfiles/
27
+
28
+ # Secrets — never commit
29
+ .env
30
+ .env.*
31
+ !.env.example
32
+ *.pem
33
+ *.key
34
+
35
+ # Editors / OS
36
+ .idea/
37
+ .vscode/
38
+ .DS_Store
39
+ Thumbs.db
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [YEAR] [COPYRIGHT HOLDER]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: university-creator
3
+ Version: 0.1.0
4
+ Summary: University Creator: creator profiles, verification, linked social accounts, media kits, campaigns, sponsorships and analytics for the University platform.
5
+ Project-URL: Homepage, https://github.com/NzeStan/University
6
+ Project-URL: Documentation, https://github.com/NzeStan/University/blob/main/docs/modules/creator.md
7
+ Project-URL: Source, https://github.com/NzeStan/University
8
+ Project-URL: Changelog, https://github.com/NzeStan/University/blob/main/CHANGELOG.md
9
+ Author-email: Ifeanyi Stanley <nnamaniifeanyi10@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: africa,campaigns,creator,django,drf,influencer,media-kit,sponsorship,university
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Framework :: Django
15
+ Classifier: Framework :: Django :: 5.2
16
+ Classifier: Framework :: Django :: 6.0
17
+ Classifier: Framework :: Django :: 6.1
18
+ Classifier: Intended Audience :: Developers
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: university-core<0.2,>=0.1
28
+ Provides-Extra: test
29
+ Requires-Dist: drf-spectacular>=0.27; extra == 'test'
30
+ Requires-Dist: pytest-cov>=5; extra == 'test'
31
+ Requires-Dist: pytest-django>=4.8; extra == 'test'
32
+ Requires-Dist: pytest>=8; extra == 'test'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # university-creator
36
+
37
+ The creator economy for the [University platform](https://github.com/NzeStan/University):
38
+ creator profiles, verification, linked social accounts, media kits, sponsorship campaigns and
39
+ analytics. This is an optional *level two* module that depends only on `university-core`.
40
+
41
+ - **Profiles** follow each user's own privacy settings and blocks. They carry categories, a
42
+ verified badge on the public profile, and a discovery list with filters (category, country,
43
+ verified, reach, open to sponsorships).
44
+ - **Verification.** Requests are reviewed by someone other than the creator; decisions are audited
45
+ and can be revoked.
46
+ - **Linked accounts** (TikTok, Instagram, YouTube, X, Facebook, Spotify, Audiomack, Boomplay …):
47
+ - Every platform sits behind a `SocialProvider` adapter.
48
+ - Ownership is proven with a bio challenge code or confirmed by a reviewer.
49
+ - Only confirmed follower counts count toward a creator's reach.
50
+ - A verified account can belong to only one creator, which prevents impersonation.
51
+ - No platform passwords or tokens are stored.
52
+ - **Media kits** have public, unlisted (revocable link) or private visibility, and hold validated
53
+ audience breakdowns, a rate card, highlights and live platform stats.
54
+ - **Campaigns and sponsorships:**
55
+ - Brands invite creators, or creators apply; brands shortlist and accept.
56
+ - Slots are enforced in the database, and requirements (verified only, minimum reach) are
57
+ checked on both paths.
58
+ - Agreed fees, deliverables with review, and ad-disclosure confirmation are all supported.
59
+ - Campaigns opt in to moderation, with reports going to the sponsor's moderators.
60
+ - **Analytics** are daily aggregates only (followers, reactions, comments, reposts, mentions,
61
+ profile and media kit views, external follower growth). Views are de-duplicated without storing
62
+ who viewed.
63
+
64
+ ```bash
65
+ pip install university-creator
66
+ ```
67
+
68
+ ```python
69
+ INSTALLED_APPS += ["university.creator"]
70
+ ```
71
+
72
+ See [the module documentation](../../docs/modules/creator.md).
@@ -0,0 +1,38 @@
1
+ # university-creator
2
+
3
+ The creator economy for the [University platform](https://github.com/NzeStan/University):
4
+ creator profiles, verification, linked social accounts, media kits, sponsorship campaigns and
5
+ analytics. This is an optional *level two* module that depends only on `university-core`.
6
+
7
+ - **Profiles** follow each user's own privacy settings and blocks. They carry categories, a
8
+ verified badge on the public profile, and a discovery list with filters (category, country,
9
+ verified, reach, open to sponsorships).
10
+ - **Verification.** Requests are reviewed by someone other than the creator; decisions are audited
11
+ and can be revoked.
12
+ - **Linked accounts** (TikTok, Instagram, YouTube, X, Facebook, Spotify, Audiomack, Boomplay …):
13
+ - Every platform sits behind a `SocialProvider` adapter.
14
+ - Ownership is proven with a bio challenge code or confirmed by a reviewer.
15
+ - Only confirmed follower counts count toward a creator's reach.
16
+ - A verified account can belong to only one creator, which prevents impersonation.
17
+ - No platform passwords or tokens are stored.
18
+ - **Media kits** have public, unlisted (revocable link) or private visibility, and hold validated
19
+ audience breakdowns, a rate card, highlights and live platform stats.
20
+ - **Campaigns and sponsorships:**
21
+ - Brands invite creators, or creators apply; brands shortlist and accept.
22
+ - Slots are enforced in the database, and requirements (verified only, minimum reach) are
23
+ checked on both paths.
24
+ - Agreed fees, deliverables with review, and ad-disclosure confirmation are all supported.
25
+ - Campaigns opt in to moderation, with reports going to the sponsor's moderators.
26
+ - **Analytics** are daily aggregates only (followers, reactions, comments, reposts, mentions,
27
+ profile and media kit views, external follower growth). Views are de-duplicated without storing
28
+ who viewed.
29
+
30
+ ```bash
31
+ pip install university-creator
32
+ ```
33
+
34
+ ```python
35
+ INSTALLED_APPS += ["university.creator"]
36
+ ```
37
+
38
+ See [the module documentation](../../docs/modules/creator.md).
@@ -0,0 +1,57 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.24"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "university-creator"
7
+ dynamic = ["version"]
8
+ description = "University Creator: creator profiles, verification, linked social accounts, media kits, campaigns, sponsorships and analytics for the University platform."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.11"
13
+ authors = [{ name = "Ifeanyi Stanley", email = "nnamaniifeanyi10@gmail.com" }]
14
+ keywords = ["django", "drf", "university", "creator", "influencer", "sponsorship", "media-kit", "campaigns", "africa"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: Django",
18
+ "Framework :: Django :: 5.2",
19
+ "Framework :: Django :: 6.0",
20
+ "Framework :: Django :: 6.1",
21
+ "Intended Audience :: Developers",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Typing :: Typed",
29
+ ]
30
+ # Level-two modules depend only on Core (same minor series).
31
+ dependencies = ["university-core>=0.1,<0.2"]
32
+
33
+ [project.optional-dependencies]
34
+ test = ["pytest>=8", "pytest-django>=4.8", "pytest-cov>=5", "drf-spectacular>=0.27"]
35
+
36
+ [project.urls]
37
+ Homepage = "https://github.com/NzeStan/University"
38
+ Documentation = "https://github.com/NzeStan/University/blob/main/docs/modules/creator.md"
39
+ Source = "https://github.com/NzeStan/University"
40
+ Changelog = "https://github.com/NzeStan/University/blob/main/CHANGELOG.md"
41
+
42
+ [tool.hatch.version]
43
+ path = "src/university/creator/__init__.py"
44
+
45
+ [tool.hatch.build.targets.wheel]
46
+ packages = ["src/university"]
47
+ exclude = ["**/__pycache__"]
48
+
49
+ [tool.hatch.build.targets.sdist]
50
+ include = ["src/university", "tests", "README.md", "LICENSE", "pyproject.toml"]
51
+
52
+ [tool.pytest.ini_options]
53
+ DJANGO_SETTINGS_MODULE = "tests.settings"
54
+ pythonpath = ["."]
55
+ testpaths = ["tests"]
56
+ addopts = "-ra --strict-markers"
57
+ markers = ["security: security regression tests"]
@@ -0,0 +1,15 @@
1
+ """University Creator — creator profiles, verification, media kits, campaigns and analytics.
2
+
3
+ An optional *level two* module that depends only on University Core::
4
+
5
+ pip install university-creator
6
+
7
+ INSTALLED_APPS = [..., *CORE_APPS, "university.creator"]
8
+
9
+ External platforms (TikTok, Instagram, YouTube, X, Facebook, Spotify …) are
10
+ reached only through :class:`university.creator.providers.SocialProvider`
11
+ adapters, configured per deployment. Analytics are daily aggregates built
12
+ from ids-only platform events; no viewer-level records are kept.
13
+ """
14
+
15
+ __version__ = "0.1.0"
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from django.contrib import admin
6
+
7
+ from university.creator.models import Campaign, CreatorCategory, CreatorProfile, LinkedAccount, VerificationRequest
8
+
9
+
10
+ @admin.register(CreatorCategory)
11
+ class CreatorCategoryAdmin(admin.ModelAdmin):
12
+ list_display = ("name", "key", "is_active", "position")
13
+ list_editable = ("is_active", "position")
14
+
15
+
16
+ class LinkedAccountInline(admin.TabularInline):
17
+ model = LinkedAccount
18
+ extra = 0
19
+ fields = ("provider", "handle", "ownership", "follower_count", "count_source", "last_synced_at")
20
+ readonly_fields = ("ownership", "count_source", "last_synced_at")
21
+
22
+
23
+ @admin.register(CreatorProfile)
24
+ class CreatorProfileAdmin(admin.ModelAdmin):
25
+ list_display = ("user", "headline", "verification_status", "verified_reach", "status")
26
+ list_filter = ("verification_status", "status", "categories")
27
+ search_fields = ("user__handle", "user__display_name", "headline")
28
+ raw_id_fields = ("user",)
29
+ # Verification changes go through the review workflow (audited), not the admin.
30
+ readonly_fields = ("verification_status", "verified_at", "verified_reach")
31
+ inlines = (LinkedAccountInline,)
32
+
33
+
34
+ @admin.register(VerificationRequest)
35
+ class VerificationRequestAdmin(admin.ModelAdmin):
36
+ list_display = ("profile", "status", "created_at", "reviewed_by", "reviewed_at")
37
+ list_filter = ("status",)
38
+ raw_id_fields = ("profile", "reviewed_by")
39
+ readonly_fields = ("status", "reviewed_by", "reviewed_at", "decision_reason")
40
+
41
+ def has_add_permission(self, request: Any) -> bool:
42
+ return False
43
+
44
+
45
+ @admin.register(Campaign)
46
+ class CampaignAdmin(admin.ModelAdmin):
47
+ list_display = ("title", "brand_name", "status", "visibility", "accepted_count", "slots", "hidden_at")
48
+ list_filter = ("status", "visibility")
49
+ search_fields = ("title", "brand_name")
50
+ raw_id_fields = ("owner",)
51
+ readonly_fields = ("accepted_count", "published_at", "closed_at", "hidden_at", "hidden_reason")
52
+
53
+ def get_queryset(self, request: Any) -> Any:
54
+ return Campaign.all_objects.all()
@@ -0,0 +1,374 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from rest_framework import serializers
6
+
7
+ from university.core.api.schema import extend_schema_field
8
+ from university.core.api.serializers import StrictModelSerializer
9
+ from university.creator.constants import CampaignVisibility, KitVisibility, ProfileStatus
10
+ from university.creator.models import (
11
+ Campaign,
12
+ CreatorCategory,
13
+ CreatorProfile,
14
+ Deliverable,
15
+ LinkedAccount,
16
+ MediaKit,
17
+ Participation,
18
+ VerificationRequest,
19
+ )
20
+ from university.creator.providers import providers
21
+
22
+
23
+ class CreatorPersonSerializer(serializers.Serializer):
24
+ id = serializers.UUIDField()
25
+ handle = serializers.CharField(allow_null=True)
26
+ display_name = serializers.CharField()
27
+
28
+
29
+ def person(user: Any) -> dict[str, Any] | None:
30
+ if user is None or not user.is_active:
31
+ return None
32
+ return {"id": user.pk, "handle": user.handle, "display_name": user.display_name or user.handle or ""}
33
+
34
+
35
+ class CreatorCategorySerializer(StrictModelSerializer):
36
+ class Meta:
37
+ model = CreatorCategory
38
+ fields = ("key", "name")
39
+ read_only_fields = fields
40
+
41
+
42
+ class CreatorProviderSerializer(serializers.Serializer):
43
+ key = serializers.CharField()
44
+ label = serializers.CharField()
45
+ automatic_verification = serializers.BooleanField()
46
+
47
+
48
+ def provider_list() -> list[dict[str, Any]]:
49
+ return [
50
+ {"key": key, "label": p.label, "automatic_verification": p.supports_lookup} for key, p in providers().items()
51
+ ]
52
+
53
+
54
+ # ---------------------------------------------------------------- accounts
55
+ class CreatorLinkedAccountSerializer(StrictModelSerializer):
56
+ """Public view of a linked account."""
57
+
58
+ class Meta:
59
+ model = LinkedAccount
60
+ fields = ("id", "provider", "handle", "url", "display_name", "follower_count", "count_source", "ownership")
61
+ read_only_fields = fields
62
+
63
+
64
+ class CreatorOwnLinkedAccountSerializer(CreatorLinkedAccountSerializer):
65
+ """The owner's view: includes the challenge code to place in their bio."""
66
+
67
+ class Meta(CreatorLinkedAccountSerializer.Meta):
68
+ fields = (
69
+ *CreatorLinkedAccountSerializer.Meta.fields,
70
+ "challenge",
71
+ "is_public",
72
+ "verified_at",
73
+ "last_synced_at",
74
+ )
75
+ read_only_fields = fields
76
+
77
+
78
+ class CreatorLinkAccountSerializer(serializers.Serializer):
79
+ provider = serializers.CharField(max_length=32)
80
+ handle = serializers.CharField(max_length=100)
81
+ follower_count = serializers.IntegerField(min_value=0, required=False, allow_null=True)
82
+
83
+
84
+ class CreatorAccountUpdateSerializer(serializers.Serializer):
85
+ follower_count = serializers.IntegerField(min_value=0, required=False, allow_null=True)
86
+ is_public = serializers.BooleanField(required=False, allow_null=True)
87
+
88
+
89
+ class CreatorConfirmAccountSerializer(serializers.Serializer):
90
+ follower_count = serializers.IntegerField(min_value=0, required=False, allow_null=True)
91
+
92
+
93
+ # ---------------------------------------------------------------- profiles
94
+ class CreatorProfileSerializer(StrictModelSerializer):
95
+ user = serializers.SerializerMethodField()
96
+ categories = CreatorCategorySerializer(many=True, read_only=True)
97
+ linked_accounts = serializers.SerializerMethodField()
98
+ is_verified = serializers.BooleanField(read_only=True)
99
+
100
+ class Meta:
101
+ model = CreatorProfile
102
+ fields = (
103
+ "id",
104
+ "user",
105
+ "headline",
106
+ "bio",
107
+ "categories",
108
+ "city",
109
+ "country",
110
+ "languages",
111
+ "open_to_sponsorships",
112
+ "business_email",
113
+ "is_verified",
114
+ "verification_status",
115
+ "verified_at",
116
+ "verified_reach",
117
+ "linked_accounts",
118
+ "created_at",
119
+ )
120
+ read_only_fields = fields
121
+
122
+ @extend_schema_field(CreatorPersonSerializer(allow_null=True))
123
+ def get_user(self, obj: CreatorProfile) -> dict[str, Any] | None:
124
+ return person(obj.user)
125
+
126
+ @extend_schema_field(CreatorLinkedAccountSerializer(many=True))
127
+ def get_linked_accounts(self, obj: CreatorProfile) -> list[dict[str, Any]]:
128
+ return CreatorLinkedAccountSerializer(obj.linked_accounts.filter(is_public=True), many=True).data
129
+
130
+
131
+ class CreatorOwnProfileSerializer(CreatorProfileSerializer):
132
+ class Meta(CreatorProfileSerializer.Meta):
133
+ fields = (*CreatorProfileSerializer.Meta.fields, "status", "is_discoverable")
134
+ read_only_fields = fields
135
+
136
+
137
+ class CreatorProfileWriteSerializer(serializers.Serializer):
138
+ headline = serializers.CharField(max_length=120, required=False, allow_blank=True)
139
+ bio = serializers.CharField(max_length=5000, required=False, allow_blank=True)
140
+ categories = serializers.ListField(child=serializers.CharField(max_length=40), required=False, max_length=10)
141
+ city = serializers.CharField(max_length=100, required=False, allow_blank=True)
142
+ country = serializers.RegexField(r"^[A-Za-z]{2}$", required=False, allow_blank=True)
143
+ languages = serializers.ListField(child=serializers.CharField(max_length=10), required=False, max_length=10)
144
+ is_discoverable = serializers.BooleanField(required=False)
145
+ open_to_sponsorships = serializers.BooleanField(required=False)
146
+ business_email = serializers.EmailField(required=False, allow_blank=True)
147
+ status = serializers.ChoiceField(choices=ProfileStatus.choices, required=False)
148
+
149
+
150
+ # ------------------------------------------------------------ verification
151
+ class CreatorVerificationRequestSerializer(StrictModelSerializer):
152
+ profile = serializers.SerializerMethodField()
153
+
154
+ class Meta:
155
+ model = VerificationRequest
156
+ fields = ("id", "profile", "status", "statement", "links", "decision_reason", "reviewed_at", "created_at")
157
+ read_only_fields = fields
158
+
159
+ @extend_schema_field(CreatorPersonSerializer(allow_null=True))
160
+ def get_profile(self, obj: VerificationRequest) -> dict[str, Any] | None:
161
+ return person(obj.profile.user)
162
+
163
+
164
+ class CreatorVerificationCreateSerializer(serializers.Serializer):
165
+ statement = serializers.CharField(max_length=3000)
166
+ links = serializers.ListField(child=serializers.CharField(max_length=300), required=False, max_length=10)
167
+
168
+
169
+ class CreatorReasonSerializer(serializers.Serializer):
170
+ reason = serializers.CharField(max_length=500, required=False, allow_blank=True, default="")
171
+
172
+
173
+ # --------------------------------------------------------------- media kit
174
+ class CreatorMediaKitSerializer(StrictModelSerializer):
175
+ profile = serializers.SerializerMethodField()
176
+ platform_stats = serializers.SerializerMethodField()
177
+
178
+ class Meta:
179
+ model = MediaKit
180
+ fields = ("profile", "summary", "audience", "rates", "highlights", "platform_stats", "updated_at")
181
+ read_only_fields = fields
182
+
183
+ @extend_schema_field(CreatorProfileSerializer)
184
+ def get_profile(self, obj: MediaKit) -> dict[str, Any]:
185
+ return CreatorProfileSerializer(obj.profile).data
186
+
187
+ def get_platform_stats(self, obj: MediaKit) -> dict[str, Any] | None:
188
+ return self.context.get("platform_stats") if obj.show_platform_stats else None
189
+
190
+
191
+ class CreatorOwnMediaKitSerializer(CreatorMediaKitSerializer):
192
+ class Meta(CreatorMediaKitSerializer.Meta):
193
+ fields = (*CreatorMediaKitSerializer.Meta.fields, "visibility", "share_token", "show_platform_stats")
194
+ read_only_fields = fields
195
+
196
+
197
+ class CreatorMediaKitWriteSerializer(serializers.Serializer):
198
+ visibility = serializers.ChoiceField(choices=KitVisibility.choices, required=False)
199
+ summary = serializers.CharField(max_length=5000, required=False, allow_blank=True)
200
+ audience = serializers.JSONField(required=False)
201
+ rates = serializers.JSONField(required=False)
202
+ highlights = serializers.JSONField(required=False)
203
+ show_platform_stats = serializers.BooleanField(required=False)
204
+
205
+
206
+ # --------------------------------------------------------------- campaigns
207
+ class CreatorScopeRefSerializer(serializers.Serializer):
208
+ type = serializers.CharField()
209
+ id = serializers.CharField()
210
+
211
+
212
+ class CreatorCampaignSerializer(StrictModelSerializer):
213
+ categories = CreatorCategorySerializer(many=True, read_only=True)
214
+ sponsor = serializers.SerializerMethodField()
215
+ slots_left = serializers.SerializerMethodField()
216
+ viewer = serializers.SerializerMethodField()
217
+
218
+ class Meta:
219
+ model = Campaign
220
+ fields = (
221
+ "id",
222
+ "brand_name",
223
+ "title",
224
+ "brief",
225
+ "categories",
226
+ "sponsor",
227
+ "status",
228
+ "visibility",
229
+ "slots",
230
+ "slots_left",
231
+ "budget_minor",
232
+ "currency",
233
+ "deliverables_summary",
234
+ "application_deadline",
235
+ "starts_on",
236
+ "ends_on",
237
+ "verified_only",
238
+ "min_verified_reach",
239
+ "disclosure_required",
240
+ "published_at",
241
+ "viewer",
242
+ "created_at",
243
+ )
244
+ read_only_fields = fields
245
+
246
+ @extend_schema_field(CreatorScopeRefSerializer(allow_null=True))
247
+ def get_sponsor(self, obj: Campaign) -> dict[str, str] | None:
248
+ return {"type": obj.scope_type, "id": obj.scope_id} if obj.scope_type else None
249
+
250
+ def get_slots_left(self, obj: Campaign) -> int | None:
251
+ return None if obj.slots is None else max(obj.slots - obj.accepted_count, 0)
252
+
253
+ @extend_schema_field(serializers.DictField())
254
+ def get_viewer(self, obj: Campaign) -> dict[str, Any]:
255
+ participation = (self.context.get("participations") or {}).get(obj.pk)
256
+ return {
257
+ "can_manage": bool((self.context.get("managed") or {}).get(obj.pk)),
258
+ "participation_id": participation.pk if participation else None,
259
+ "participation_status": participation.status if participation else None,
260
+ }
261
+
262
+
263
+ class CreatorCampaignWriteSerializer(serializers.Serializer):
264
+ brand_name = serializers.CharField(max_length=120)
265
+ title = serializers.CharField(max_length=160)
266
+ brief = serializers.CharField(max_length=10000)
267
+ categories = serializers.ListField(child=serializers.CharField(max_length=40), required=False, max_length=10)
268
+ visibility = serializers.ChoiceField(choices=CampaignVisibility.choices, required=False)
269
+ slots = serializers.IntegerField(min_value=1, required=False, allow_null=True)
270
+ budget_minor = serializers.IntegerField(min_value=0, required=False, allow_null=True)
271
+ currency = serializers.RegexField(r"^[A-Z]{3}$", required=False)
272
+ deliverables_summary = serializers.CharField(max_length=5000, required=False, allow_blank=True)
273
+ application_deadline = serializers.DateTimeField(required=False, allow_null=True)
274
+ starts_on = serializers.DateField(required=False, allow_null=True)
275
+ ends_on = serializers.DateField(required=False, allow_null=True)
276
+ verified_only = serializers.BooleanField(required=False)
277
+ min_verified_reach = serializers.IntegerField(min_value=0, required=False)
278
+ disclosure_required = serializers.BooleanField(required=False)
279
+ sponsor_type = serializers.CharField(max_length=32, required=False, allow_blank=True, default="", write_only=True)
280
+ sponsor_id = serializers.CharField(max_length=64, required=False, allow_blank=True, default="", write_only=True)
281
+
282
+
283
+ class CreatorParticipationSerializer(StrictModelSerializer):
284
+ creator = serializers.SerializerMethodField()
285
+ campaign = serializers.SerializerMethodField()
286
+
287
+ class Meta:
288
+ model = Participation
289
+ fields = (
290
+ "id",
291
+ "campaign",
292
+ "creator",
293
+ "status",
294
+ "pitch",
295
+ "proposed_fee_minor",
296
+ "agreed_fee_minor",
297
+ "fee_paid_at",
298
+ "note",
299
+ "decided_at",
300
+ "created_at",
301
+ )
302
+ read_only_fields = fields
303
+
304
+ @extend_schema_field(serializers.DictField())
305
+ def get_creator(self, obj: Participation) -> dict[str, Any]:
306
+ profile = obj.profile
307
+ return {
308
+ "profile_id": profile.pk,
309
+ "user": person(profile.user),
310
+ "is_verified": profile.is_verified,
311
+ "verified_reach": profile.verified_reach,
312
+ }
313
+
314
+ @extend_schema_field(serializers.DictField())
315
+ def get_campaign(self, obj: Participation) -> dict[str, Any]:
316
+ campaign = obj.campaign
317
+ return {
318
+ "id": campaign.pk,
319
+ "title": campaign.title,
320
+ "brand_name": campaign.brand_name,
321
+ "currency": campaign.currency,
322
+ }
323
+
324
+
325
+ class CreatorApplySerializer(serializers.Serializer):
326
+ pitch = serializers.CharField(max_length=3000)
327
+ proposed_fee_minor = serializers.IntegerField(min_value=0, required=False, allow_null=True)
328
+
329
+
330
+ class CreatorInviteSerializer(serializers.Serializer):
331
+ profile_id = serializers.UUIDField()
332
+ note = serializers.CharField(max_length=500, required=False, allow_blank=True, default="")
333
+
334
+
335
+ class CreatorDecisionSerializer(serializers.Serializer):
336
+ action = serializers.ChoiceField(choices=["shortlist", "accept", "decline", "complete", "cancel", "withdraw"])
337
+ fee_minor = serializers.IntegerField(min_value=0, required=False, allow_null=True)
338
+ note = serializers.CharField(max_length=500, required=False, allow_blank=True, default="")
339
+
340
+
341
+ class CreatorDeliverableSerializer(StrictModelSerializer):
342
+ class Meta:
343
+ model = Deliverable
344
+ fields = (
345
+ "id",
346
+ "title",
347
+ "description",
348
+ "due_at",
349
+ "status",
350
+ "submission_url",
351
+ "submission_note",
352
+ "disclosure_confirmed",
353
+ "submitted_at",
354
+ "reviewed_at",
355
+ "review_note",
356
+ )
357
+ read_only_fields = fields
358
+
359
+
360
+ class CreatorDeliverableWriteSerializer(serializers.Serializer):
361
+ title = serializers.CharField(max_length=160)
362
+ description = serializers.CharField(max_length=5000, required=False, allow_blank=True)
363
+ due_at = serializers.DateTimeField(required=False, allow_null=True)
364
+
365
+
366
+ class CreatorSubmitSerializer(serializers.Serializer):
367
+ url = serializers.URLField(max_length=500)
368
+ note = serializers.CharField(max_length=2000, required=False, allow_blank=True, default="")
369
+ disclosure_confirmed = serializers.BooleanField(default=False)
370
+
371
+
372
+ class CreatorReviewSerializer(serializers.Serializer):
373
+ approve = serializers.BooleanField()
374
+ note = serializers.CharField(max_length=1000, required=False, allow_blank=True, default="")