diff --git a/README.md b/README.md new file mode 100644 index 0000000..4dcefba --- /dev/null +++ b/README.md @@ -0,0 +1,104 @@ + +# MVP CRM-системы для Амбассадоров Яндекс Практикума. + +## О проекте + +Проект команды №10 в хакатоне Яндекс-Практикума. + +## Команда разработчиков + +- [Рагимов Шериф](https://github.com/ragimov700) +- [Зеленчук Михаил](https://github.com/qwertttyyy) +- [Земцов Антон](https://github.com/antonata-c) +- [Гуржий Вадим](https://github.com/VadimGurzhy) + +## Технологии +[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=white)](https://www.djangoproject.com/) +[![DjangoRESTFramework](https://img.shields.io/badge/Django%20REST%20Framework-092E20?style=for-the-badge&logo=django&logoColor=white)](https://www.django-rest-framework.org/) +[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-316192?style=for-the-badge&logo=postgresql&logoColor=white)](https://www.postgresql.org) +[![Docker](https://img.shields.io/badge/Docker-0db7ed?style=for-the-badge&logo=docker&logoColor=white)](https://www.docker.com) +[![Nginx](https://img.shields.io/badge/nginx-009639?style=for-the-badge&logo=nginx&logoColor=white)](https://www.nginx.org) +![CI/CD](https://img.shields.io/badge/CI%2FCD-2088FF?style=for-the-badge&logo=GitHub-Actions&logoColor=white) + + +## Начало работы + +Эти инструкции позволят вам запустить копию проекта на вашем локальном компьютере для разработки и тестирования. + +
+Запуск с использованием Docker + +### Предварительные требования + +Убедитесь, что у вас установлены Docker и Docker Compose. Это можно сделать, следуя официальной документации Docker: https://docs.docker.com/get-docker/ и https://docs.docker.com/compose/install/ + +### Установка и запуск + +1. Клонируйте репозиторий на локальный компьютер: + ``` + git clone git@github.com:Tenth-Team/backend.git + cd backend/infra + ``` + +2. Запустите контейнеры с помощью Docker Compose: + ``` + docker-compose up --build + ``` + + Теперь приложение должно быть доступно по адресу: + + http://localhost:8000 + + А документация доступна по адресу: + + http://localhost:8000/api/v1/swagger/ + +
+ +
+Локальный запуск через pip + +### Предварительные требования + +Убедитесь, что у вас установлен Python и pip. Рекомендуется использовать виртуальное окружение для изоляции зависимостей проекта. + +### Установка и запуск + +1. Клонируйте репозиторий на локальный компьютер: + ``` + git clone git@github.com:Tenth-Team/backend.git + cd backend/backend + ``` + +2. Создайте и активируйте виртуальное окружение: + ``` + python -m venv venv + source venv/bin/activate # На Windows используйте `venv\Scripts\activate` + ``` + +3. Установите зависимости: + ``` + pip install -r requirements.txt + ``` + +4. Запустите проект (пример для Django): + ``` + python manage.py migrate + python manage.py runserver + ``` + + Теперь приложение должно быть доступно по адресу: + + http://localhost:8000 + + А документация доступна по адресу: + + http://localhost:8000/api/v1/swagger/ + + +
+ +### Ссылка на скриншот документации: + +[![Static Badge](https://img.shields.io/badge/Документация_Swagger-Google_Drive-blue?style=for-the-badge)](https://drive.google.com/file/d/1ySTNXQUQZt4djonFki1h1biCSBdJHsIO/view) + diff --git a/backend/ambassadors/admin.py b/backend/ambassadors/admin.py index 2496210..beaea7f 100644 --- a/backend/ambassadors/admin.py +++ b/backend/ambassadors/admin.py @@ -15,26 +15,41 @@ @admin.register(Country) class CountryAdmin(admin.ModelAdmin): + """ + Конфигурация админки для модели стран. + """ list_display = ('name',) @admin.register(City) class CityAdmin(admin.ModelAdmin): + """ + Конфигурация админки для модели города. + """ list_display = ('name',) @admin.register(TrainingProgram) class TrainingProgramAdmin(admin.ModelAdmin): + """ + Конфигурация админки для модели программ обучения. + """ list_display = ('name',) @admin.register(AmbassadorGoal) class AmbassadorGoalAdmin(admin.ModelAdmin): + """ + Конфигурация админки для модели цели амбассадорства. + """ list_display = ('name',) @admin.register(Ambassador) class AmbassadorAdmin(admin.ModelAdmin): + """ + Конфигурация админки для модели амбассадора. + """ list_display = ( 'full_name', 'gender', @@ -50,6 +65,9 @@ class AmbassadorAdmin(admin.ModelAdmin): @admin.register(Content) class ContentAdmin(admin.ModelAdmin): + """ + Конфигурация админки для модели контента. + """ list_display = ( 'id', 'full_name', diff --git a/backend/ambassadors/factories.py b/backend/ambassadors/factories.py index 002a4f8..8bf0577 100644 --- a/backend/ambassadors/factories.py +++ b/backend/ambassadors/factories.py @@ -23,10 +23,11 @@ TrainingProgram, ) -fake = Faker(['en-US', 'en_US', 'en_US', 'en-US']) - class CityFactory(DjangoModelFactory): + """ + Фабрика для модели городов. + """ class Meta: model = City @@ -34,6 +35,9 @@ class Meta: class CountryFactory(DjangoModelFactory): + """ + Фабрика для модели стран. + """ class Meta: model = Country @@ -41,6 +45,9 @@ class Meta: class TrainingProgramFactory(DjangoModelFactory): + """ + Фабрика для модели программ обучения. + """ class Meta: model = TrainingProgram @@ -48,6 +55,9 @@ class Meta: class AmbassadorGoalFactory(DjangoModelFactory): + """ + Фабрика для модели целей амбассадорства. + """ class Meta: model = AmbassadorGoal @@ -55,6 +65,9 @@ class Meta: class AmbassadorFactory(DjangoModelFactory): + """ + Фабрика для модели амбассадора. + """ class Meta: model = Ambassador @@ -89,6 +102,9 @@ class Meta: class ContentFactory(DjangoModelFactory): + """ + Фабрика для модели контента. + """ class Meta: model = Content @@ -105,6 +121,9 @@ class Meta: class PromoCodeFactory(DjangoModelFactory): + """ + Фабрика для модели промокодов. + """ class Meta: model = PromoCode @@ -117,6 +136,9 @@ class Meta: class MerchandiseFactory(DjangoModelFactory): + """ + Фабрика для модели мерча. + """ class Meta: model = Merchandise @@ -131,6 +153,9 @@ class Meta: class MerchandiseShippingRequestFactory(DjangoModelFactory): + """ + Фабрика для модели заявок на мерч. + """ class Meta: model = MerchandiseShippingRequest diff --git a/backend/ambassadors/signals.py b/backend/ambassadors/signals.py index a0c0452..3be38f5 100644 --- a/backend/ambassadors/signals.py +++ b/backend/ambassadors/signals.py @@ -5,6 +5,10 @@ @receiver(pre_save, sender=PromoCode) def make_other_promo_codes_inactive(sender, instance, **kwargs): + """ + Сигнал, который деактивирует все остальные промокоды + при определении активного. + """ if instance.status == 'active': PromoCode.objects.filter( ambassador=instance.ambassador diff --git a/backend/api/v1/filters.py b/backend/api/v1/filters.py index 315a864..d362ffb 100644 --- a/backend/api/v1/filters.py +++ b/backend/api/v1/filters.py @@ -14,8 +14,9 @@ class ContentFilter(FilterSet): - """Класс для фильтрации Контента.""" - + """ + Фильтры для контента. + """ status = ChoiceFilter(choices=CONTENT_STATUS_CHOICES) full_name = CharFilter(field_name='full_name', lookup_expr='icontains') ya_edu = CharFilter( diff --git a/backend/api/v1/pagination.py b/backend/api/v1/pagination.py index 314c83e..961d1e8 100644 --- a/backend/api/v1/pagination.py +++ b/backend/api/v1/pagination.py @@ -2,9 +2,15 @@ class AmbassadorPagination(LimitOffsetPagination): + """ + Пагинация для амбассадоров. + """ default_limit = 30 max_limit = 100 class ContentPagination(AmbassadorPagination): + """ + Пагинация для контента. + """ pass diff --git a/backend/api/v1/permissions.py b/backend/api/v1/permissions.py index fc11f52..b3e7db3 100644 --- a/backend/api/v1/permissions.py +++ b/backend/api/v1/permissions.py @@ -3,6 +3,10 @@ class IsAuthenticatedOrYandexForms(permissions.BasePermission): + """ + Пермишен, который разрешает действия только для авторизованных + пользователей или Яндекс формам. + """ def has_permission(self, request, view): api_key = request.headers.get('Authorization') return (request.user.is_authenticated diff --git a/backend/api/v1/serializers.py b/backend/api/v1/serializers.py index 2020d9a..da3403b 100644 --- a/backend/api/v1/serializers.py +++ b/backend/api/v1/serializers.py @@ -221,6 +221,9 @@ class Meta: class AmbassadorUpdateSerializer(serializers.ModelSerializer): + """ + Сериализатор для изменения амбассадора. + """ city = serializers.CharField(required=False) country = serializers.CharField(required=False) diff --git a/backend/api/v1/views.py b/backend/api/v1/views.py index 7eb3380..03879c6 100644 --- a/backend/api/v1/views.py +++ b/backend/api/v1/views.py @@ -197,6 +197,9 @@ class ContentViewSet(viewsets.ModelViewSet): @extend_schema(tags=["Заявки"]) @extend_schema_view(**merch_schema) class MerchandiseShippingRequestViewSet(viewsets.ModelViewSet): + """ + Viewset для модели заявки на мерч. + """ queryset = MerchandiseShippingRequest.objects.all() serializer_class = MerchandiseShippingRequestSerializer http_method_names = ( @@ -220,8 +223,10 @@ def download(self, request): @extend_schema(tags=["Программы и цели"], **loyalty_schema) class AmbassadorLoyaltyViewSet(ListAPIView): - """Viewset для получения данных для страницы лояльности. - Возвращает список амбассадоров.""" + """ + Viewset для получения данных для страницы лояльности. + Возвращает список амбассадоров. + """ shipped_merch_prefetch = Prefetch( 'merch_shipping_requests', diff --git a/backend/config/settings.py b/backend/config/settings.py index d65f85a..ad32a08 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -8,7 +8,7 @@ DEBUG = os.environ.get('DEBUG', 'True').lower() == 'true' ALLOWED_HOSTS = os.getenv("ALLOWED_HOSTS", '*').split() -CSRF_TRUSTED_ORIGINS = ['https://crm.ragimov700.ru'] +CSRF_TRUSTED_ORIGINS = ['https://crm.ragimov700.ru', 'http://127.0.0.1:8000'] INSTALLED_APPS = [ 'django.contrib.admin', diff --git a/infra/.env.example b/infra/.env.example new file mode 100644 index 0000000..4e197b8 --- /dev/null +++ b/infra/.env.example @@ -0,0 +1,9 @@ +DEBUG=True +SECRET_KEY=django-insecure-ssksrb!2)bv^nd%9*=^w=^0h$=0_r-^mb6xo@9yi!)u1d2%)uv +ALLOWED_HOSTS=* + +POSTGRES_DB='postgres' +POSTGRES_USER='admin' +POSTGRES_PASSWORD='password' +DB_HOST='db' +DB_PORT=5432 \ No newline at end of file diff --git a/infra/docker-compose.local.yml b/infra/docker-compose.local.yml index 6879e97..57ce661 100644 --- a/infra/docker-compose.local.yml +++ b/infra/docker-compose.local.yml @@ -1,16 +1,37 @@ version: '3.3' services: + db: + image: postgres:13-alpine + volumes: + - postgres:/var/lib/postgresql/data + env_file: + - .env.example + restart: always backend: build: ../backend - env_file: ../.env + env_file: + - .env.example volumes: - ../backend/:/app/ + - backend_static:/app/static + command: > + sh -c "python manage.py collectstatic --no-input && + sleep 3 && + python manage.py migrate && + python manage.py runserver 0.0.0.0:8000" + depends_on: + - db nginx: image: nginx:1.19.3 volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf + - backend_static:/backend_static + - ./nginx.conf:/etc/nginx/conf.d/default.conf ports: - - "80:80" + - "8000:80" depends_on: - backend + +volumes: + postgres: + backend_static: diff --git a/infra/docker-compose.production.yml b/infra/docker-compose.production.yml index fc0cd7d..85dac3d 100644 --- a/infra/docker-compose.production.yml +++ b/infra/docker-compose.production.yml @@ -2,20 +2,36 @@ version: '3.3' volumes: backend_static: + postgres_data: services: + db: + image: postgres:13-alpine + volumes: + - postgres_data:/var/lib/postgresql/data + env_file: + - .env.example + restart: always + backend: image: tenthteam/backend - command: daphne -b 0.0.0.0 -p 8000 config.asgi:application - env_file: ./.env + command: sh -c "daphne -b 0.0.0.0 -p 8000 config.asgi:application" + env_file: .env.example volumes: - backend_static:/app/static + depends_on: + - db + frontend: + image: tenthteam/frontend + volumes: + - ../frontend/:/app/result_build/ nginx: image: nginx:1.19.3 volumes: - backend_static:/backend_static - ./nginx.conf:/etc/nginx/conf.d/default.conf + - ../frontend/dist:/usr/share/nginx/html/ ports: - - "80:80" + - "8000:80" depends_on: - - backend + - backend \ No newline at end of file diff --git a/infra/nginx.conf b/infra/nginx.conf index 548fd4f..a08bee2 100644 --- a/infra/nginx.conf +++ b/infra/nginx.conf @@ -1,18 +1,39 @@ server { listen 80; - + server_name 80.90.186.109; server_tokens off; location /backend_static/ { alias /backend_static/; } - location / { + location /api/ { proxy_pass http://backend:8000; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_redirect off; } -} \ No newline at end of file + location /admin/ { + proxy_pass http://backend:8000/admin/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri /index.html; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /var/html/frontend/; + } +}