[chore] Global project refactoring

This commit is contained in:
ITQ
2024-04-01 11:20:07 +03:00
parent 0a8b3773f5
commit 5c64e1f3b9
49 changed files with 731 additions and 496 deletions
View File
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api"
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class CoreConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "core"
+14
View File
@@ -0,0 +1,14 @@
from django.db import models
class AbstractTag(models.Model):
name = models.CharField(
max_length=255,
unique=True,
)
class Meta:
abstract = True
def __str__(self):
return self.name
@@ -0,0 +1,31 @@
from django.contrib import admin
from notifications import models
from notifications.forms import (
CreateNotificationAdminForm,
EditNotificationAdminForm,
)
class NotificationAdmin(admin.ModelAdmin):
form = EditNotificationAdminForm
add_form = CreateNotificationAdminForm
list_display = [
models.Notification.title.field.name,
models.Notification.user.field.name,
models.Notification.content.field.name,
models.Notification.read.field.name,
models.Notification.created_at.field.name,
]
def get_readonly_fields(self, request, obj=None): # noqa: ARG002
if obj:
return (
*self.readonly_fields,
models.Notification.read.field.name,
models.Notification.created_at.field.name,
)
return self.readonly_fields
admin.site.register(models.Notification, NotificationAdmin)
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class NotificationsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "notifications"
@@ -0,0 +1,22 @@
from django import forms
from notifications.models import Notification
class EditNotificationAdminForm(forms.ModelForm):
class Meta:
model = Notification
fields = (
model.user.field.name,
model.title.field.name,
model.content.field.name,
)
class CreateNotificationAdminForm(forms.ModelForm):
class Meta:
model = Notification
fields = (
model.user.field.name,
model.title.field.name,
model.content.field.name,
)
@@ -0,0 +1,36 @@
from django.conf import settings
from django.db import models
class NotificationManager(models.Manager):
def by_user(self, user_id):
return self.get_queryset().filter(
user__id=user_id,
)
class Notification(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notifications",
)
title = models.CharField(
max_length=150,
null=False,
)
content = models.TextField(
verbose_name="content",
null=False,
)
read = models.BooleanField(
default=False,
)
created_at = models.DateTimeField(
auto_now_add=True,
)
objects = NotificationManager()
def __str__(self):
return self.title
@@ -0,0 +1,8 @@
from notifications.models import Notification
from rest_framework import serializers
class NotificationSerializer(serializers.ModelSerializer):
class Meta:
model = Notification
fields = ["title", "content", "read", "created_at"]
@@ -0,0 +1,13 @@
from notifications.models import Notification
from notifications.serializers import NotificationSerializer
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
class UserNotificationsAPIView(generics.ListAPIView):
serializer_class = NotificationSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
user = self.request.user
return Notification.objects.by_user(user.id)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class PingConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api.ping"
+7
View File
@@ -0,0 +1,7 @@
from django.urls import path
import api.ping.views
urlpatterns = [
path("", api.ping.views.PingApiView.as_view(), name="ping"),
]
+9
View File
@@ -0,0 +1,9 @@
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
class PingApiView(APIView):
def get(self, request): # noqa: ARG002
data = "ok"
return Response(data, status=status.HTTP_200_OK)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TeamsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "teams"
+78
View File
@@ -0,0 +1,78 @@
from django.core import validators
from django.db import models
from api.users.models import Skill, Specialization, User
class Vacancy(models.Model):
name = models.CharField(
max_length=255,
)
age_restriction = models.DateField(
blank=True,
null=True,
)
specialization = models.ForeignKey(
Specialization,
on_delete=models.CASCADE,
blank=True,
null=True,
)
skills = models.ManyToManyField(
Skill,
blank=True,
)
def __str__(self):
return self.name
class Team(models.Model):
name = models.CharField(max_length=255)
description = models.TextField()
members = models.ManyToManyField(
User,
blank=True,
unique=True,
)
vacancies = models.ManyToManyField(
Vacancy,
blank=True,
unique=True,
)
avatar = models.ImageField(
upload_to="teams_avatars",
blank=True,
)
count_of_members = models.IntegerField(
validators=[
validators.MinValueValidator(1),
validators.MaxLengthValidator(5),
],
verbose_name="количество участников",
null=True,
)
country = models.CharField(
blank=True,
max_length=255,
verbose_name="страна",
)
city = models.CharField(
blank=True,
max_length=255,
verbose_name="город",
)
author = models.ForeignKey(
User,
on_delete=models.CASCADE,
)
def __str__(self):
return self.name
+8
View File
@@ -0,0 +1,8 @@
from rest_framework import serializers
from teams.models import Team
class TeamSerializer(serializers.ModelSerializer):
class Meta:
model = Team
fields = ["id", "name", "description"]
+11
View File
@@ -0,0 +1,11 @@
from django.urls import path
from .views import AddUserToTeam
urlpatterns = [
path(
"add_user_to_team/<int:team_id>/<int:user_id>/",
AddUserToTeam.as_view(),
name="add_user_to_team",
),
]
+26
View File
@@ -0,0 +1,26 @@
from backend.project.users.models import User
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from teams.models import Team
from .serializers import TeamSerializer
class AddUserToTeam(APIView):
def post(self, request, team_id, user_id): # noqa: ARG002
try:
team = Team.objects.get(id=team_id)
user = User.objects.get(id=user_id)
except Team.DoesNotExist:
return Response(
{"error": "Team not found"}, status=status.HTTP_404_NOT_FOUND
)
except User.DoesNotExist:
return Response(
{"error": "User not found"}, status=status.HTTP_404_NOT_FOUND
)
team.members.add(user)
team_serializer = TeamSerializer(team)
return Response(team_serializer.data, status=status.HTTP_200_OK)
+6
View File
@@ -0,0 +1,6 @@
from django.urls import include, path
urlpatterns = [
path("ping", include("api.ping.urls")),
path("auth", include("api.users.urls")),
]
+5
View File
@@ -0,0 +1,5 @@
from django.contrib import admin
from api.users.models import User
admin.site.register(User)
+6
View File
@@ -0,0 +1,6 @@
from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "api.users"
@@ -0,0 +1,51 @@
import bcrypt
import jwt
from django.conf import settings
from rest_framework.authentication import (
BaseAuthentication,
)
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated
from rest_framework.permissions import IsAuthenticated
from api.users.models import User
class JWTAuthentication(BaseAuthentication):
def authenticate_header(self, request): # noqa: ARG002
return "Provide a valid token in the 'Authorization' header"
def authenticate(self, request):
if IsAuthenticated not in getattr(
request.resolver_match.func.cls, "permission_classes", []
):
return None
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
if not token:
raise NotAuthenticated
try:
payload = jwt.decode(
token, settings.SECRET_KEY, algorithms=["HS256"]
)
user = User.objects.get(id=payload["id"])
if not bcrypt.checkpw(
payload["password"].encode("utf-8"),
user.password.encode("utf-8"),
):
error = "Token has expired"
raise AuthenticationFailed(error)
except User.DoesNotExist:
error = "Invalid token"
raise AuthenticationFailed(error) from None
except jwt.ExpiredSignatureError:
error = "Token has expired"
raise AuthenticationFailed(error) from None
except jwt.InvalidTokenError:
error = "Invalid token"
raise AuthenticationFailed(error) from None
else:
return (user, None)
@@ -0,0 +1,238 @@
# Generated by Django 4.2.11 on 2024-04-01 01:58
import api.users.models
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
class Migration(migrations.Migration):
initial = True
dependencies = [
("auth", "0012_alter_user_first_name_max_length"),
]
operations = [
migrations.CreateModel(
name="Achievements",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(
upload_to=api.users.models.Achievements.get_file_path
),
),
("info", models.TextField(max_length=255)),
],
),
migrations.CreateModel(
name="Skill",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
(
"level",
models.IntegerField(
validators=[
django.core.validators.MinValueValidator(1),
django.core.validators.MaxValueValidator(10),
]
),
),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="Specialization",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=255, unique=True)),
],
options={
"abstract": False,
},
),
migrations.CreateModel(
name="User",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("password", models.CharField(max_length=128, verbose_name="password")),
(
"last_login",
models.DateTimeField(
blank=True, null=True, verbose_name="last login"
),
),
(
"is_superuser",
models.BooleanField(
default=False,
help_text="Designates that this user has all permissions without explicitly assigning them.",
verbose_name="superuser status",
),
),
(
"username",
models.CharField(
error_messages={
"unique": "A user with that username already exists."
},
help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.",
max_length=150,
unique=True,
validators=[
django.contrib.auth.validators.UnicodeUsernameValidator()
],
verbose_name="username",
),
),
(
"first_name",
models.CharField(
blank=True, max_length=150, verbose_name="first name"
),
),
(
"last_name",
models.CharField(
blank=True, max_length=150, verbose_name="last name"
),
),
(
"is_staff",
models.BooleanField(
default=False,
help_text="Designates whether the user can log into this admin site.",
verbose_name="staff status",
),
),
(
"is_active",
models.BooleanField(
default=True,
help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.",
verbose_name="active",
),
),
(
"date_joined",
models.DateTimeField(
default=django.utils.timezone.now, verbose_name="date joined"
),
),
("email", models.EmailField(max_length=254, unique=True)),
("birthday", models.DateField(blank=True, null=True)),
(
"avatar",
models.ImageField(
max_length=200,
null=True,
upload_to=api.users.models.User.get_file_path,
),
),
("country", models.TextField(blank=True)),
("city", models.TextField(blank=True)),
(
"experience",
models.IntegerField(
null=True,
validators=[
django.core.validators.MinValueValidator(0),
django.core.validators.MinValueValidator(100),
],
),
),
(
"bio",
models.TextField(
blank=True,
validators=[django.core.validators.MaxLengthValidator(512)],
),
),
(
"achievements",
models.ManyToManyField(blank=True, to="users.achievements"),
),
(
"groups",
models.ManyToManyField(
blank=True,
help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.",
related_name="user_set",
related_query_name="user",
to="auth.group",
verbose_name="groups",
),
),
("skills", models.ManyToManyField(blank=True, to="users.skill")),
(
"specialization",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="users.specialization",
),
),
(
"user_permissions",
models.ManyToManyField(
blank=True,
help_text="Specific permissions for this user.",
related_name="user_set",
related_query_name="user",
to="auth.permission",
verbose_name="user permissions",
),
),
],
options={
"verbose_name": "user",
"verbose_name_plural": "users",
"abstract": False,
},
managers=[
("objects", django.contrib.auth.models.UserManager()),
],
),
]
+94
View File
@@ -0,0 +1,94 @@
import uuid
from django.contrib.auth.models import AbstractUser
from django.core import validators
from django.db import models
from api.core.models import AbstractTag
class Skill(AbstractTag):
level = models.IntegerField(
validators=[
validators.MinValueValidator(1),
validators.MaxValueValidator(10),
],
)
class Achievements(models.Model):
def get_file_path(self, filename):
folder_name = str(uuid.uuid4())
return f"achievements/{folder_name}/{filename}"
file = models.FileField( # noqa: DJ012
upload_to=get_file_path,
)
info = models.TextField(
max_length=255,
)
def __str__(self): # noqa: DJ012
return self.info
class Specialization(AbstractTag):
pass
class User(AbstractUser):
def get_file_path(self, filename):
folder_name = str(uuid.uuid4())
return f"avatars/{folder_name}/{filename}"
email = models.EmailField(unique=True)
birthday = models.DateField(
blank=True,
null=True,
)
avatar = models.ImageField(
upload_to=get_file_path,
max_length=200,
null=True,
)
country = models.TextField(
blank=True,
)
city = models.TextField(
blank=True,
)
experience = models.IntegerField(
validators=[
validators.MinValueValidator(0),
validators.MinValueValidator(100),
],
null=True,
)
bio = models.TextField(
blank=True,
validators=[
validators.MaxLengthValidator(
512,
),
],
)
skills = models.ManyToManyField(
Skill,
blank=True,
)
achievements = models.ManyToManyField(
Achievements,
blank=True,
)
specialization = models.ForeignKey(
Specialization,
on_delete=models.SET_NULL,
blank=True,
null=True,
)
def __str__(self):
return self.username
+79
View File
@@ -0,0 +1,79 @@
from django.contrib.auth.password_validation import validate_password
from rest_framework import serializers
from api.users.models import User
class UserRegistrationSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"first_name",
"last_name",
"username",
"email",
"password",
"country",
"city",
]
def validate_password(self, value):
validate_password(value)
return value
class UserLoginSerializer(serializers.Serializer):
remember_me = serializers.BooleanField(default=False, required=False)
username = serializers.CharField()
password = serializers.CharField()
class UserProfileSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = (
"first_name",
"last_name",
"username",
"email",
"birthday",
"country",
"city",
"bio",
"avatar",
"experience",
"specialization",
"achievements",
"skills",
)
class UpdateProfileSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = [
"countryCode",
"isPublic",
"phone",
"image",
]
def to_representation(self, instance):
data = super().to_representation(instance)
if data["image"] is None:
del data["image"]
if data["phone"] is None:
del data["phone"]
return data
class PasswordChangeSerializer(serializers.Serializer):
# ruff: noqa: N815
old_password = serializers.CharField(required=True)
new_password = serializers.CharField(required=True)
def validate_password(self, value):
validate_password(value)
return value
+26
View File
@@ -0,0 +1,26 @@
from django.urls import path
import api.users.views
urlpatterns = [
path(
"/sign-up/",
api.users.views.SignupUserApiView.as_view(),
name="sign-up",
),
path(
"/sign-in/",
api.users.views.SigninUserApiView.as_view(),
name="sign-in",
),
path(
"/me/profile/",
api.users.views.ProfileMeApiView.as_view(),
name="profile-me",
),
path(
"/me/updatePassword/",
api.users.views.PasswordChangeApiView.as_view(),
name="password-change",
),
]
+131
View File
@@ -0,0 +1,131 @@
from datetime import timedelta
import bcrypt
import jwt
from django.conf import settings
from django.utils import timezone
from rest_framework import status
from rest_framework.exceptions import (
NotAuthenticated,
PermissionDenied,
ValidationError,
)
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from api.users.models import User
from api.users.serializers import (
PasswordChangeSerializer,
UpdateProfileSerializer,
UserLoginSerializer,
UserProfileSerializer,
UserRegistrationSerializer,
)
class SignupUserApiView(APIView):
def post(self, request):
serializer = UserRegistrationSerializer(data=request.data)
if serializer.is_valid():
password = serializer.validated_data["password"]
password_hash = bcrypt.hashpw(
password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
serializer.validated_data["password"] = password_hash
serializer.save()
return Response("ok", status=status.HTTP_201_CREATED)
raise ValidationError(serializer.errors)
class SigninUserApiView(APIView):
def post(self, request):
serializer = UserLoginSerializer(data=request.data)
if not serializer.is_valid():
raise ValidationError(serializer.errors)
username = serializer.validated_data.get("username")
password = serializer.validated_data.get("password")
user = User.objects.filter(username=username).first()
if user is not None:
if not bcrypt.checkpw(
password.encode("utf-8"), user.password.encode("utf-8")
):
raise NotAuthenticated(
{"error": "Invalid credentials"},
)
else:
raise NotAuthenticated(
{"error": "Invalid credentials"},
)
token = jwt.encode(
{
"id": user.id,
"password": password,
"exp": timezone.now() + timedelta(hours=24),
},
settings.SECRET_KEY,
algorithm="HS256",
)
return Response({"token": token})
class ProfileMeApiView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
serializer = UserProfileSerializer(request.user)
return Response(serializer.data)
def patch(self, request):
user = request.user
serializer = UpdateProfileSerializer(
user, data=request.data, partial=True
)
if serializer.is_valid():
errors = User.check_unique(user.id, serializer.validated_data)
if errors:
return Response(
{"reason:": str(errors)}, status=status.HTTP_409_CONFLICT
)
serializer.save()
return Response(self._get_profile_data(user))
raise ValidationError(serializer.errors)
class PasswordChangeApiView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
serializer = PasswordChangeSerializer(data=request.data)
if serializer.is_valid():
old_password = serializer.validated_data.get("oldPassword")
new_password = serializer.validated_data.get("newPassword")
if bcrypt.checkpw(
old_password.encode("utf-8"),
request.user.password.encode("utf-8"),
):
password_hash = bcrypt.hashpw(
new_password.encode("utf-8"), bcrypt.gensalt()
).decode("utf-8")
request.user.password = password_hash
request.user.save()
return Response({"status": "ok"}, status=status.HTTP_200_OK)
raise PermissionDenied({"error": "Invalid old password"})
raise ValidationError(serializer.errors)