-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
c4ffein
committed
Jun 16, 2024
1 parent
5b6c7cc
commit 45e472e
Showing
16 changed files
with
624 additions
and
289 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
# | ||
# c4ffein's default .gitignore v0 | ||
# https://gist.github.com/c4ffein/58c1dba80e5665d85112f3eab513a170 | ||
# | ||
# Copyright (c) 2021 c4ffein | ||
# https://raw.githubusercontent.com/c4ffein/c4ffein/main/legal_files/mit_c4ffein_2021.txt | ||
|
||
# Web stuff | ||
node_modules | ||
## Ghost stuff | ||
.ghostpid | ||
ghost-local.db | ||
|
||
# C extensions | ||
*.so | ||
|
||
# Distribution / packaging | ||
.Python | ||
env/ | ||
build/ | ||
develop-eggs/ | ||
dist/ | ||
downloads/ | ||
eggs/ | ||
.eggs/ | ||
lib/ | ||
lib64/ | ||
parts/ | ||
sdist/ | ||
var/ | ||
*.egg-info/ | ||
.installed.cfg | ||
*.egg | ||
|
||
# Unit test / coverage reports | ||
htmlcov/ | ||
.tox/ | ||
.coverage | ||
.coverage.* | ||
.cache | ||
nosetests.xml | ||
coverage.xml | ||
*,cover | ||
.hypothesis/ | ||
|
||
# Translations | ||
*.mo | ||
*.pot | ||
|
||
# Python stuff | ||
# Byte-compiled / optimized | ||
__pycache__/ | ||
*.py[cod] | ||
*$py.class | ||
## Django stuff: | ||
local_settings.py | ||
*.log | ||
## Flask stuff: | ||
instance/ | ||
.webassets-cache | ||
## Scrapy stuff: | ||
.scrapy | ||
## Sphinx documentation | ||
docs/_build/ | ||
## PyBuilder | ||
target/ | ||
## IPython Notebook | ||
.ipynb_checkpoints | ||
## pyenv | ||
.python-version | ||
## virtualenv | ||
.env | ||
venv/ | ||
ENV/ | ||
## Spyder project settings | ||
.spyderproject | ||
## Rope project settings | ||
.ropeproject | ||
## Installer logs | ||
pip-log.txt | ||
pip-delete-this-directory.txt | ||
pip* | ||
|
||
# DB stuff | ||
db.sqlite3 | ||
|
||
# Editors | ||
.vscode/ | ||
|
||
# Mac stuff | ||
.DS_Store | ||
|
||
# Custom | ||
notes.md | ||
notes.txt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
from typing import Any | ||
|
||
from django.contrib.auth import authenticate | ||
from django.conf import settings | ||
from django.db import IntegrityError, transaction | ||
from django.shortcuts import get_object_or_404 | ||
from ninja import Router | ||
from rest_framework_simplejwt.tokens import RefreshToken | ||
from rest_framework_simplejwt.tokens import AccessToken | ||
|
||
from accounts.schemas import ( | ||
EMPTY, | ||
ProfileSchema, | ||
UserCreateSchema, | ||
UserGetSchema, | ||
UserInPartialUpdateOutSchema, | ||
UserLoginSchema, | ||
UserMineSchema, | ||
UserPartialUpdateInSchema, | ||
UserPartialUpdateOutSchema, | ||
) | ||
from accounts.models import User | ||
from helpers.auth import AuthJWT | ||
from helpers.exceptions import clean_integrity_error | ||
|
||
|
||
router = Router() | ||
|
||
|
||
@router.post('/users', response={201: Any, 400: Any, 409: Any}) | ||
def account_registration(request, data: UserCreateSchema): | ||
try: | ||
user = User.objects.create_user(data.user.email, username=data.user.username, password=data.user.password) | ||
except IntegrityError as err: | ||
return 409, {"already_existing": clean_integrity_error(err)} | ||
jwt_token = AccessToken.for_user(user) | ||
return 201, { | ||
'user': { | ||
'username': user.username, | ||
'email': user.email, | ||
'bio': user.bio or None, | ||
'image': user.image or settings.DEFAULT_USER_IMAGE, | ||
'token': str(jwt_token), | ||
}, | ||
} | ||
|
||
|
||
@router.post('/users/login', response={200: Any, 401: Any}) | ||
def account_login(request, data: UserLoginSchema): | ||
user = authenticate(email=data.user.email, password=data.user.password) | ||
if user is None: | ||
return 401, {'detail': [{'msg': 'incorrect credentials'}]} | ||
jwt_token = AccessToken.for_user(user) | ||
return { | ||
'user': { | ||
'username': user.username, | ||
'email': user.email, | ||
'bio': user.bio or None, | ||
'image': user.image or settings.DEFAULT_USER_IMAGE, | ||
'token': str(jwt_token), | ||
}, | ||
} | ||
|
||
|
||
@router.get('/user', auth=AuthJWT(), response={200: Any, 404: Any}) | ||
def get_user(request) -> UserGetSchema: | ||
return {"user": UserMineSchema.from_orm(request.user)} | ||
|
||
|
||
@router.put('/user', auth=AuthJWT(), response={200: Any, 401: Any}) | ||
def put_user(request, data: UserPartialUpdateInSchema) -> UserPartialUpdateOutSchema: | ||
"""This is wrong, but this method behaves like a PATCH, as required by the RealWorld API spec""" | ||
for word in ("email", "bio", "image", "username"): | ||
value = getattr(data.user, word) | ||
if value is not EMPTY: | ||
setattr(request.user, word, value) | ||
if data.user.password is not EMPTY: | ||
request.user.set_password(data.user.password) | ||
request.user.save() | ||
token = AccessToken.for_user(request.user) | ||
return {"user": UserInPartialUpdateOutSchema.from_orm(request.user, context={"token": token})} | ||
|
||
|
||
@router.get('/profiles/{username}', auth=AuthJWT(pass_even=True), response={200: Any, 401: Any, 404: Any}) | ||
def get_profiles(request, username: str): | ||
return { | ||
"profile": ProfileSchema.from_orm(get_object_or_404(User, username=username), context={"request": request}) | ||
} | ||
|
||
|
||
@router.post('/profiles/{username}/follow', auth=AuthJWT(), response={200: Any, 400: Any, 404: Any}) | ||
def follow_profile(request, username: str): | ||
profile = get_object_or_404(User, username=username) | ||
if profile == request.user: | ||
return 400, {"errors": {"body": ["Invalid follow Request"]}} | ||
profile.followers.add(request.user) | ||
return {"profile": ProfileSchema.from_orm(profile, context={"request": request})} | ||
|
||
|
||
@router.delete('/profiles/{username}/follow', auth=AuthJWT(), response={200: Any, 400: Any, 404: Any}) | ||
def unfollow_profile(request, username: str): | ||
profile = get_object_or_404(User, username=username) | ||
if profile == request.user: | ||
return 400, {"errors": {"body": ["Invalid follow Request"]}} | ||
if not profile.followers.filter(pk=request.user.id).exists(): | ||
return 400, {"errors": {"body": ["Invalid follow Request"]}} | ||
profile.followers.remove(request.user) | ||
return {"profile": ProfileSchema.from_orm(profile, context={"request": request})} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Generated by Django 4.2.2 on 2024-06-10 19:31 | ||
|
||
from django.db import migrations, models | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
dependencies = [ | ||
('accounts', '0002_rename_name_user_username'), | ||
] | ||
|
||
operations = [ | ||
migrations.AlterField( | ||
model_name='user', | ||
name='username', | ||
field=models.CharField(max_length=60, unique=True), | ||
), | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
from typing import Annotated, Optional, Union | ||
|
||
from django.conf import settings | ||
from ninja import Field, ModelSchema, Schema | ||
from pydantic import AfterValidator, ConfigDict, EmailStr, field_validator, ValidationInfo | ||
|
||
from accounts.models import User | ||
|
||
|
||
EMPTY = object() | ||
|
||
|
||
def none_to_blank(v: Optional[str], info: ValidationInfo) -> str: | ||
return "" if v is None else v | ||
|
||
|
||
class ProfileSchema(ModelSchema): | ||
following: bool | ||
bio: Optional[str] | ||
image: str | ||
|
||
class Meta: | ||
model = User | ||
fields = ["username"] | ||
|
||
@staticmethod | ||
def resolve_following(obj, context) -> str: | ||
user = context.get("request").user | ||
return obj.followers.filter(pk=user.id).exists() if user.is_authenticated else False | ||
|
||
@staticmethod | ||
def resolve_bio(obj, context) -> str: | ||
return obj.bio or None | ||
|
||
@staticmethod | ||
def resolve_image(obj, context) -> str: | ||
return obj.image or settings.DEFAULT_USER_IMAGE | ||
|
||
|
||
class UserInCreateSchema(ModelSchema): | ||
email: EmailStr | ||
|
||
class Meta: | ||
model = User | ||
fields = ["email", "password", "username"] | ||
|
||
@field_validator("email", "password", "username", check_fields=False) | ||
@classmethod | ||
def non_empty(cls, v: str) -> str: | ||
if not v: | ||
raise ValueError("can't be blank") | ||
return v | ||
|
||
|
||
class UserCreateSchema(Schema): | ||
user: UserInCreateSchema | ||
|
||
|
||
class UserInLoginSchema(ModelSchema): | ||
class Meta: | ||
model = User | ||
fields = ["email", "password"] | ||
|
||
@field_validator("email", "password", check_fields=False) | ||
@classmethod | ||
def non_empty(cls, v: str) -> str: | ||
if not v: | ||
raise ValueError("can't be blank") | ||
return v | ||
|
||
|
||
class UserLoginSchema(Schema): | ||
user: UserInLoginSchema | ||
|
||
|
||
class UserMineSchema(ModelSchema): | ||
email: EmailStr | ||
|
||
class Meta: | ||
model = User | ||
fields = ["email", "bio", "image", "username"] | ||
|
||
|
||
class UserGetSchema(Schema): | ||
user: UserMineSchema | ||
|
||
|
||
class UserInPartialUpdateInSchema(Schema): | ||
email: Annotated[Optional[EmailStr], AfterValidator(none_to_blank)] = EMPTY | ||
bio: Annotated[Optional[str], AfterValidator(none_to_blank)] = EMPTY | ||
image: Annotated[Optional[str], AfterValidator(none_to_blank)] = EMPTY | ||
username: Annotated[Optional[str], AfterValidator(none_to_blank)] = EMPTY | ||
password: Annotated[Optional[str], AfterValidator(none_to_blank)] = EMPTY | ||
|
||
|
||
class UserPartialUpdateInSchema(Schema): | ||
user: UserInPartialUpdateInSchema | ||
|
||
|
||
class UserInPartialUpdateOutSchema(UserMineSchema): | ||
token: str | ||
|
||
@staticmethod | ||
def resolve_token(obj, context) -> str: | ||
return str(context.get("token", "") if context is not None else "") | ||
|
||
|
||
class UserPartialUpdateOutSchema(Schema): | ||
user: UserInPartialUpdateOutSchema |
Oops, something went wrong.