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.
- university_creator-0.1.0/.gitignore +39 -0
- university_creator-0.1.0/LICENSE +21 -0
- university_creator-0.1.0/PKG-INFO +72 -0
- university_creator-0.1.0/README.md +38 -0
- university_creator-0.1.0/pyproject.toml +57 -0
- university_creator-0.1.0/src/university/creator/__init__.py +15 -0
- university_creator-0.1.0/src/university/creator/admin.py +54 -0
- university_creator-0.1.0/src/university/creator/api/__init__.py +0 -0
- university_creator-0.1.0/src/university/creator/api/v1/__init__.py +0 -0
- university_creator-0.1.0/src/university/creator/api/v1/serializers.py +374 -0
- university_creator-0.1.0/src/university/creator/api/v1/urls.py +80 -0
- university_creator-0.1.0/src/university/creator/api/v1/views.py +569 -0
- university_creator-0.1.0/src/university/creator/apps.py +127 -0
- university_creator-0.1.0/src/university/creator/conf.py +71 -0
- university_creator-0.1.0/src/university/creator/constants.py +101 -0
- university_creator-0.1.0/src/university/creator/content.py +92 -0
- university_creator-0.1.0/src/university/creator/events.py +20 -0
- university_creator-0.1.0/src/university/creator/handlers.py +53 -0
- university_creator-0.1.0/src/university/creator/integrations.py +87 -0
- university_creator-0.1.0/src/university/creator/management/__init__.py +0 -0
- university_creator-0.1.0/src/university/creator/management/commands/__init__.py +0 -0
- university_creator-0.1.0/src/university/creator/management/commands/sync_creator_accounts.py +20 -0
- university_creator-0.1.0/src/university/creator/migrations/0001_initial.py +289 -0
- university_creator-0.1.0/src/university/creator/migrations/0002_participation_fee_payment.py +23 -0
- university_creator-0.1.0/src/university/creator/migrations/__init__.py +0 -0
- university_creator-0.1.0/src/university/creator/models.py +296 -0
- university_creator-0.1.0/src/university/creator/payables.py +66 -0
- university_creator-0.1.0/src/university/creator/providers.py +153 -0
- university_creator-0.1.0/src/university/creator/selectors.py +162 -0
- university_creator-0.1.0/src/university/creator/services/__init__.py +69 -0
- university_creator-0.1.0/src/university/creator/services/accounts.py +223 -0
- university_creator-0.1.0/src/university/creator/services/analytics.py +114 -0
- university_creator-0.1.0/src/university/creator/services/campaigns.py +495 -0
- university_creator-0.1.0/src/university/creator/services/profiles.py +246 -0
- university_creator-0.1.0/src/university/creator/services/verification.py +125 -0
- university_creator-0.1.0/src/university/creator/tasks.py +5 -0
- university_creator-0.1.0/tests/__init__.py +0 -0
- university_creator-0.1.0/tests/audience/__init__.py +0 -0
- university_creator-0.1.0/tests/audience/apps.py +28 -0
- university_creator-0.1.0/tests/conftest.py +22 -0
- university_creator-0.1.0/tests/fakes.py +20 -0
- university_creator-0.1.0/tests/settings.py +9 -0
- university_creator-0.1.0/tests/test_creator.py +536 -0
- 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()
|
|
File without changes
|
|
File without changes
|
|
@@ -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="")
|