Merge pull request #1 from Central-University-IT-prod/main

Added CI/CD and global project refactoring
This commit is contained in:
ITQ
2024-04-01 11:46:53 +03:00
committed by GitHub
57 changed files with 2801 additions and 634 deletions
+25 -14
View File
@@ -3,17 +3,18 @@ name: Django CI/CD
on: [push, pull_request]
jobs:
checking:
migrations:
runs-on: ubuntu-latest
env:
DJANGO_DEBUG: True
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Copy env file
run: cp backend/template.env backend/.env
- name: Install production dependencies
run: pip install -r backend/requirements/prod.txt
- name: Check for pending migrations
@@ -21,6 +22,8 @@ jobs:
linting:
runs-on: ubuntu-latest
needs: migrations
steps:
- uses: actions/checkout@v3
- name: Set up Python
@@ -33,20 +36,28 @@ jobs:
run: cd backend && ruff check .
testing:
runs-on: ubuntu-latest
runs-on: self-hosted
needs: linting
env:
DJANGO_DEBUG: True
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.x
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Copy env file
run: cp backend/template.env backend/.env
- name: Install prod dependencies
run: pip install -r backend/requirements/prod.txt
- name: Test production environment
run: cd backend/project && DJANGO_DEBUG=False python manage.py test
- name: Install dev dependencies
run: pip install -r backend/requirements/dev.txt
- name: Test development environment
run: cd backend/project && DJANGO_DEBUG=True python manage.py test
- name: Install test dependencies
run: pip install -r backend/requirements/test.txt
- name:
run: cd backend/project && python manage.py test
test_build:
runs-on: ubuntu-latest
needs: testing
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t skillhub_backend backend/
+18
View File
@@ -0,0 +1,18 @@
name: Deployment
on:
workflow_run:
workflows: ["Frontend CI/CD"]
types:
- completed
jobs:
deploy:
runs-on: self-hosted
if: ${{ github.event.workflow_run.conclusion == 'success' }} && ${{ github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v3
- name: Pull Docker images and start containers
run: |
sudo docker compose up -d --build
+32
View File
@@ -0,0 +1,32 @@
name: Frontend CI/CD
on:
workflow_run:
workflows: ["Django CI/CD"]
types:
- completed
jobs:
linting:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: Install dependencies
run: cd frontend && npm install
- name: Linting
run: cd frontend && npm run lint
continue-on-error: true
test_build:
runs-on: ubuntu-latest
needs: linting
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t skillhub-backend frontend/
+1
View File
@@ -162,3 +162,4 @@ cython_debug/
# Django stuff
cache
media
static/
+4 -8
View File
@@ -1,15 +1,15 @@
dump:
@cd project && python -Xutf8 manage.py dumpdata users --format json --indent 4 -o fixtures/users.json
@cd project && python -Xutf8 manage.py dumpdata --format json --indent 4 -o fixtures/data.json
load:
@cd project && python -Xutf8 manage.py loaddata fixtures/users.json
@cd project && python -Xutf8 manage.py loaddata fixtures/data.json
mig:
@cd project && python manage.py makemigrations
@cd project && python manage.py migrate
check: test
@ruff check
@ruff check --fix
test:
@cd project && python manage.py test
@@ -29,9 +29,5 @@ loc-c:
help:
@cd project && python manage.py help
fix:
ruff check --fix
sort:
sort-requirements requirements/prod.txt requirements/test.txt requirements/dev.txt
req:
@pip install -r requirements/dev.txt
+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
@@ -1,5 +1,4 @@
from django.contrib import admin
from notifications import models
from notifications.forms import (
CreateNotificationAdminForm,
@@ -1,5 +1,4 @@
from django import forms
from notifications.models import Notification
@@ -14,31 +14,23 @@ class Notification(models.Model):
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="notifications",
verbose_name="пользователь",
)
title = models.CharField(
max_length=150,
verbose_name="заголовок",
null=False,
)
content = models.TextField(
verbose_name="содержание",
verbose_name="content",
null=False,
)
read = models.BooleanField(
"дата создания",
default=False,
)
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name="дата создания",
)
objects = NotificationManager()
class Meta:
verbose_name = "уведомление"
verbose_name_plural = "уведомления"
def __str__(self):
return self.title
@@ -1,6 +1,5 @@
from rest_framework import serializers
from notifications.models import Notification
from rest_framework import serializers
class NotificationSerializer(serializers.ModelSerializer):
@@ -1,8 +1,7 @@
from rest_framework import generics
from rest_framework.permissions import IsAuthenticated
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):
+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)
@@ -1,35 +1,26 @@
import users.models
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,
verbose_name="название вакансии",
)
start_date = models.DateField(
verbose_name="дата начала диапазона возраста участников",
blank=True,
null=True,
)
end_date = models.DateField(
verbose_name="дата конец диапазона возраста участников",
age_restriction = models.DateField(
blank=True,
null=True,
)
specialization = models.ForeignKey(
users.models.Specialization,
Specialization,
on_delete=models.CASCADE,
blank=True,
verbose_name="специализация",
null=True,
)
skills = models.ManyToManyField(
users.models.Skill,
Skill,
blank=True,
verbose_name="Технологии",
)
def __str__(self):
@@ -37,29 +28,24 @@ class Vacancy(models.Model):
class Team(models.Model):
description = models.TextField(
verbose_name="описание команды",
)
name = models.CharField(
verbose_name="название команды",
max_length=255,
)
name = models.CharField(max_length=255)
description = models.TextField()
members = models.ManyToManyField(
users.models.User,
verbose_name="участники",
User,
blank=True,
unique=True,
)
vacancies = models.ManyToManyField(
Vacancy,
verbose_name="вакансии",
blank=True,
unique=True,
)
avatar = models.ImageField(
upload_to="teams_avatars",
blank=True,
verbose_name="аватарка",
)
count_of_members = models.IntegerField(
@@ -84,9 +70,8 @@ class Team(models.Model):
)
author = models.ForeignKey(
users.models.User,
User,
on_delete=models.CASCADE,
related_name="teams",
)
def __str__(self):
@@ -1,5 +1,4 @@
from rest_framework import serializers
from teams.models import Team
@@ -1,12 +1,10 @@
from backend.project.users.models import User
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework.viewsets import ModelViewSet
from users.models import User
from teams.models import Team
from teams.serializers import TeamSerializer
from .serializers import TeamSerializer
class AddUserToTeam(APIView):
@@ -26,11 +24,3 @@ class AddUserToTeam(APIView):
team.members.add(user)
team_serializer = TeamSerializer(team)
return Response(team_serializer.data, status=status.HTTP_200_OK)
class TeamsViewSet(ModelViewSet):
http_method_names = ("get",)
queryset = Team.objects.all()
serializer_class = TeamSerializer
permission_classes = [IsAuthenticated]
+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")),
]
@@ -1,5 +1,5 @@
from django.contrib import admin
from users.models import User
from api.users.models import User
admin.site.register(User)
@@ -3,4 +3,4 @@ from django.apps import AppConfig
class UsersConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "users"
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)
+19 -17
View File
@@ -33,28 +33,22 @@ MIGRATING = len(sys.argv) > 1 and (
)
def register_debug_toolbar():
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
INSTALLED_APPS = [
# django apps
# Built-in apps
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# third party apps
# Third-party apps
"rest_framework",
"rest_framework_simplejwt",
"corsheaders",
"drf_yasg",
# project apps
"users.apps.UsersConfig",
"notifications.apps.NotificationsConfig",
"teams.apps.TeamsConfig",
# Developed apps
"api.ping.apps.PingConfig",
"api.users.apps.UsersConfig",
]
MIDDLEWARE = [
@@ -135,25 +129,33 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
LANGUAGE_CODE = "ru-ru"
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_TZ = True
USE_I18N = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "static"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
AUTH_USER_MODEL = "users.User"
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework_simplejwt.authentication.JWTAuthentication",
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend"
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"api.users.authentication.JWTAuthentication",
),
}
if DEBUG and not (TESTING or MIGRATING):
register_debug_toolbar()
APPEND_SLASH = False
CORS_ORIGIN_ALLOW_ALL = True
if DEBUG and not (TESTING or MIGRATING):
INSTALLED_APPS.append("debug_toolbar")
MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware")
+17 -41
View File
@@ -1,18 +1,9 @@
import django.conf
import django.contrib.admin
import django.urls
import rest_framework_simplejwt.views
import users.views
from django.conf import settings
from django.contrib import admin
from django.urls import include, path
from drf_yasg import openapi
from drf_yasg.views import get_schema_view
from rest_framework import permissions, routers
from teams.views import TeamsViewSet
from users.views import UserViewSet
router = routers.DefaultRouter()
router.register("users", UserViewSet)
router.register("teams", TeamsViewSet)
from rest_framework import permissions
schema_view = get_schema_view(
openapi.Info(title="SkillHub API", default_version="v1"),
@@ -22,6 +13,16 @@ schema_view = get_schema_view(
urlpatterns = [
# Built-in urls
path("admin/", admin.site.urls),
path(
"api-auth/",
include(
"rest_framework.urls",
namespace="rest_framework",
),
),
# API documentation
path(
"swagger<format>/",
schema_view.without_ui(cache_timeout=0),
@@ -37,34 +38,9 @@ urlpatterns = [
schema_view.with_ui("redoc", cache_timeout=0),
name="schema-redoc",
),
path("api/", include(router.urls)),
path("api/registration/", users.views.RegisterView.as_view()),
django.urls.path(
"api/token/",
rest_framework_simplejwt.views.TokenObtainPairView.as_view(),
name="token_obtain_pair",
),
django.urls.path(
"api/token/refresh/",
rest_framework_simplejwt.views.TokenRefreshView.as_view(),
name="token_refresh",
),
django.urls.path(
"api/token/verify/",
rest_framework_simplejwt.views.TokenVerifyView.as_view(),
name="token_verify",
),
django.urls.path("admin/", django.contrib.admin.site.urls),
# API
path("api/", include("api.urls")),
]
if django.conf.settings.DEBUG and not (
django.conf.settings.TESTING or django.conf.settings.MIGRATING
):
import debug_toolbar
urlpatterns.append(
django.urls.path(
"__debug__/",
django.urls.include(debug_toolbar.urls),
),
)
if settings.DEBUG and not (settings.TESTING or settings.MIGRATING):
urlpatterns += (path("__debug__/", include("debug_toolbar.urls")),)
@@ -1,56 +0,0 @@
# Generated by Django 4.2.11 on 2024-03-31 15:06
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Notification",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(max_length=150, verbose_name="заголовок")),
("content", models.TextField(null=True, verbose_name="содержание")),
(
"read",
models.BooleanField(default=False, verbose_name="дата создания"),
),
(
"created_at",
models.DateTimeField(
auto_now_add=True, verbose_name="дата создания"
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="notifications",
to=settings.AUTH_USER_MODEL,
verbose_name="пользователь",
),
),
],
options={
"verbose_name": "уведомление",
"verbose_name_plural": "уведомления",
},
),
]
-1
View File
@@ -1 +0,0 @@
# Create your tests here.
@@ -1,97 +0,0 @@
# Generated by Django 5.0.3 on 2024-03-31 12:22
import django.contrib.auth.models
import django.contrib.auth.validators
import django.core.validators
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models
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')),
('name', models.CharField(max_length=255, verbose_name='Название достижения')),
('info', models.TextField(max_length=255, verbose_name='Информация про достижение')),
('file', models.FileField(upload_to='achievements', verbose_name='Файл достижения')),
],
),
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)], verbose_name='уровень навыка')),
],
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='Tag',
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, verbose_name='электронная почта')),
('birthday', models.DateField(blank=True, help_text='Введите дату рождения', null=True, verbose_name='дата рождения')),
('avatar', models.ImageField(blank=True, upload_to='avatars', verbose_name='Аватарка')),
('country', models.CharField(blank=True, max_length=255, verbose_name='страна')),
('city', models.CharField(blank=True, max_length=255, verbose_name='город')),
('bio', models.TextField(blank=True, validators=[django.core.validators.MaxLengthValidator(512)], verbose_name='обо мне')),
('experience', models.IntegerField(null=True, validators=[django.core.validators.MinValueValidator(0), django.core.validators.MinValueValidator(100)], verbose_name='опыт работы')),
('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')),
('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')),
('achievements', models.ManyToManyField(blank=True, to='users.achievements', verbose_name='достижения')),
('technologies', models.ManyToManyField(blank=True, to='users.skill', verbose_name='технологии')),
('specialization', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='users.specialization', verbose_name='специализация')),
('tag', models.ManyToManyField(blank=True, to='users.tag', verbose_name='теги')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
'swappable': 'AUTH_USER_MODEL',
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]
@@ -1,18 +0,0 @@
# Generated by Django 5.0.3 on 2024-03-31 13:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.RenameField(
model_name='user',
old_name='technologies',
new_name='skills',
),
]
-128
View File
@@ -1,128 +0,0 @@
from django.contrib.auth.models import AbstractUser
from django.core import validators
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
class Tag(AbstractTag):
pass
class Skill(AbstractTag):
level = models.IntegerField(
validators=[
validators.MinValueValidator(1),
validators.MaxValueValidator(10),
],
verbose_name="уровень навыка",
)
class Specialization(AbstractTag):
pass
class Achievements(models.Model):
name = models.CharField(
max_length=255,
verbose_name="Название достижения",
)
info = models.TextField(
max_length=255,
verbose_name="Информация про достижение",
)
file = models.FileField(
upload_to="achievements",
verbose_name="Файл достижения",
)
def __str__(self):
return self.name
class User(AbstractUser):
email = models.EmailField("электронная почта", unique=True)
birthday = models.DateField(
verbose_name="дата рождения",
help_text="Введите дату рождения",
blank=True,
null=True,
)
avatar = models.ImageField(
upload_to="avatars",
blank=True,
verbose_name="Аватарка",
)
country = models.CharField(
blank=True,
max_length=255,
verbose_name="страна",
)
city = models.CharField(
blank=True,
max_length=255,
verbose_name="город",
)
bio = models.TextField(
blank=True,
validators=[
validators.MaxLengthValidator(
512,
),
],
verbose_name="обо мне",
)
skills = models.ManyToManyField(
Skill,
blank=True,
verbose_name="технологии",
)
tag = models.ManyToManyField(
Tag,
blank=True,
verbose_name="теги",
)
experience = models.IntegerField(
validators=[
validators.MinValueValidator(0),
validators.MinValueValidator(100),
],
verbose_name="опыт работы",
null=True,
)
achievements = models.ManyToManyField(
Achievements,
blank=True,
verbose_name="достижения",
)
specialization = models.ForeignKey(
Specialization,
on_delete=models.CASCADE,
blank=True,
verbose_name="специализация",
null=True,
)
-26
View File
@@ -1,26 +0,0 @@
import rest_framework.serializers
import users.models
class UserSerializer(rest_framework.serializers.ModelSerializer):
class Meta:
model = users.models.User
fields = (
"email",
"birthday",
"country",
"city",
"bio",
"avatar",
"password",
"first_name",
"last_name",
"tag",
"specialization",
"experience",
"achievements",
"username",
)
extra_kwargs = {"password": {"write_only": True}}
-47
View File
@@ -1,47 +0,0 @@
import rest_framework.generics
import rest_framework.permissions
import rest_framework.response
import rest_framework.viewsets
import users.models
import users.serializers
class UserViewSet(rest_framework.viewsets.ModelViewSet):
http_method_names = ("get",)
queryset = users.models.User.objects.all()
serializer_class = users.serializers.UserSerializer
permission_classes = [rest_framework.permissions.IsAuthenticated]
class RegisterView(rest_framework.generics.CreateAPIView):
http_method_names = ("post",)
serializer_class = users.serializers.UserSerializer
def post(self, request):
if users.models.User.objects.filter(
username=request.data.get("username"),
).exists():
return rest_framework.response.Response(
{
"username": [
"пользователь с таким именем уже существует.",
],
},
status=rest_framework.status.HTTP_409_CONFLICT,
)
serializer = self.get_serializer(data=request.data)
if serializer.is_valid():
serializer.save()
return rest_framework.response.Response(
serializer.data,
status=rest_framework.status.HTTP_201_CREATED,
)
return rest_framework.response.Response(
serializer.errors,
status=rest_framework.status.HTTP_400_BAD_REQUEST,
)
+5 -3
View File
@@ -1,4 +1,6 @@
django-debug-toolbar
black
sort-requirements
ruff==0.3.4
django-debug-toolbar==4.3.0
-r prod.txt
-r lint.txt
-r test.txt
+2
View File
@@ -9,3 +9,5 @@ django-filter==24.2
Pillow==10.2.0
drf-yasg==1.21.7
django-cors-headers
setuptools
bcrypt==4.1.2
+1
View File
@@ -0,0 +1 @@
-r prod.txt
-14
View File
@@ -51,18 +51,6 @@ services:
- "3000:3000"
restart: always
# nginx:
# container_name: nginx
# image: nginx:custom
# build: ./nginx
# restart: unless-stopped
# ports:
# - 80:80
# volumes:
# - media:/usr/src/app/media
# depends_on:
# - backend
pgadmin:
image: dpage/pgadmin4:8.4
container_name: pgadmin
@@ -82,5 +70,3 @@ volumes:
postgres_data:
redis_data:
pgadmin_data:
media:
external: true
-1
View File
@@ -4,7 +4,6 @@ module.exports = {
"es2021": true
},
"extends": [
"standard-with-typescript",
"plugin:react/recommended"
],
"overrides": [
+1980 -103
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -50,6 +50,8 @@
"@typescript-eslint/parser": "^7.2.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard-with-typescript": "19.0.1",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",