Skip to content

Commit

Permalink
accounts modification
Browse files Browse the repository at this point in the history
  • Loading branch information
c4ffein committed Jun 16, 2024
1 parent 5b6c7cc commit 45e472e
Show file tree
Hide file tree
Showing 16 changed files with 624 additions and 289 deletions.
95 changes: 95 additions & 0 deletions .gitignore
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
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
MIT License

Copyright (c) 2021 RealWorld
Copyright (c) 2024 c4ffein

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
108 changes: 108 additions & 0 deletions accounts/api.py
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})}
18 changes: 18 additions & 0 deletions accounts/migrations/0003_alter_user_username.py
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),
),
]
24 changes: 5 additions & 19 deletions accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,12 @@


class UserManager(BaseUserManager):
def create_user(
self, email: str, password: str | None = None, **other_fields
) -> User:
def create_user(self, email: str, password: str | None = None, **other_fields) -> User:
user = User(email=email, **other_fields)

if password:
user.set_password(password)
else:
user.set_unusable_password()

user.save()
return user

Expand All @@ -26,10 +22,8 @@ def create_superuser(self, email: str, password: str | None = None, **other_fiel
raise ValueError("Superuser must be assigned to is_staff=True.")
if other_fields.get("is_superuser") is not True:
raise ValueError("Superuser must be assigned to is_superuser=True.")

return self.create_user(email, password, **other_fields)




class User(AbstractUser):

Expand All @@ -38,7 +32,7 @@ class User(AbstractUser):
last_name = None

email: str = models.EmailField("Email Address", unique=True)
username: str = models.CharField(max_length=60)
username: str = models.CharField(max_length=60, unique=True)
bio: str = models.TextField(blank=True)
image: str | None = models.URLField(null=True, blank=True)

Expand All @@ -50,16 +44,8 @@ class User(AbstractUser):

objects = UserManager()


def get_full_name(self) -> str:
if self.first_name and self.last_name:
return f"{self.first_name} {self.last_name}"
else:
return self.username

return f"{self.first_name} {self.last_name}" if self.first_name and self.last_name else self.username

def get_short_name(self) -> str:
if self.first_name and self.last_name:
return f"{self.first_name[0]}{self.last_name}"
else:
return self.username
return f"{self.first_name[0]}{self.last_name}" if self.first_name and self.last_name else self.username
109 changes: 109 additions & 0 deletions accounts/schemas.py
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
Loading

0 comments on commit 45e472e

Please sign in to comment.