diff --git a/README.En.md b/README.En.md new file mode 100644 index 0000000..075274e --- /dev/null +++ b/README.En.md @@ -0,0 +1,139 @@ +🌍 Available languages: **English** | [Français](README.md) + +# Website API + +A RESTful API built with Django and Django REST Framework to manage website content, including blog features, events, and user management. + +## 📋 Prerequisites + +- Python 3.8+ +- PostgreSQL 12+ +- Redis (for caching and queues) +- pip (Python package manager) + +## 🚀 Installation + +1. **Clone the repository** + ```bash + git clone https://github.com/charles-kamga/website_api.git + cd website_api + ``` + +2. **Set up the virtual environment** + ```bash + python -m venv venv + source venv/bin/activate # On Windows: .\venv\Scripts\activate + ``` + +3. **Install dependencies** + ```bash + pip install -r requirements.txt + ``` + +4. **Configure the PostgreSQL database** + ```sql + -- Connect to PostgreSQL + sudo -u postgres psql + + -- Create the database + CREATE DATABASE django_website_db; + + -- Create a user (replace values in brackets) + CREATE USER [db_user] WITH PASSWORD '[your_password]'; + + -- Grant privileges + GRANT ALL PRIVILEGES ON DATABASE django_website_db TO [db_user]; + ``` + +5. **Configure environment variables** + ```bash + cp .env.example .env + ``` + + Edit the `.env` file with your settings: + - `DB_*`: Database connection parameters + - `SECRET_KEY`: Django secret key (generate a new one for production) + - `EMAIL_*`: SMTP configuration for emails + - `TWILLIO_*`: Twilio credentials for SMS verification (optional) + +6. **Apply migrations** + ```bash + python manage.py migrate + ``` + +7. **Create a superuser (optional)** + ```bash + python manage.py createsuperuser + ``` + +8. **Run the development server** + ```bash + python manage.py runserver + ``` + + The API will be available at: http://127.0.0.1:8000/ + The admin interface will be available at: http://127.0.0.1:8000/admin/ + +## 🏗 Project Structure + +``` +website_api/ +├── apps/ # Django applications +│ ├── blog/ # Blog article management +│ ├── events/ # Events management +│ └── users/ # User management and authentication +├── config/ # Project configuration +├── documentation/ # Additional documentation +├── middlewares/ # Custom middlewares +├── services/ # Business logic +└── website_api/ # Main project settings +``` + +## 🔧 Environment Variables + +| Variable | Description | Default Value | +|----------|-------------|----------------| +| `DEBUG` | Debug mode | `True` in development, `False` in production | +| `SECRET_KEY` | Django secret key | Must be defined in production | +| `DB_*` | Database settings | See `.env.example` | +| `EMAIL_*` | SMTP configuration | Must be set for emails | +| `REDIS_URL` | Redis connection URL | `redis://127.0.0.1:6379` | +| `TWILLIO_*` | Twilio credentials (SMS) | Optional | + +## 📚 API Documentation + +The API documentation is available at `/api/docs/` when the server is running. + +## 🧪 Running Tests + +```bash +# Run all tests +python manage.py test + +# Run tests for a specific app +python manage.py test apps.users +``` + +## 🛠Development Tools + +- **Linting**: `flake8` +- **Formatting**: `black` +- **Import sorting**: `isort` + +## 🤝 Contributing + +Contributions are welcome! Here’s how to contribute: + +1. Fork the project +2. Create a feature branch (`git checkout -b feature/my-new-feature`) +3. Commit your changes (`git commit -am 'Add new feature'`) +4. Push the branch (`git push origin feature/my-new-feature`) +5. Create a Pull Request + +## 📄 License + +This project is licensed under the MIT License – see the [LICENSE](LICENSE) file for details. + +## 📧 Contact + +For any questions, please open an issue on GitHub or contact the development team. diff --git a/README.md b/README.md index 4eb0eed..479f8e3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +🌍 Langues disponibles : [English](README.En.md) | **Français** + # Website API Une API RESTful construite avec Django et Django REST Framework pour gérer le contenu d'un site web, incluant des fonctionnalités de blog, d'événements et de gestion d'utilisateurs. diff --git a/apps/events/models/__init__.py b/apps/events/models/__init__.py index d427aae..753ea5f 100644 --- a/apps/events/models/__init__.py +++ b/apps/events/models/__init__.py @@ -1,3 +1,3 @@ -from .event import Event, EventCity, EventRegion, EventVenue, EventTag -from .reservation import Reservation -from .speaker import Speaker, SpeakerSocialMedia, SpeakerSpeciality, AvailableSocialMedia +from .event import Event, EventCity, EventRegion, EventVenue, EventTag +from .reservation import Reservation +from .speaker import Speaker, SpeakerSocialMedia, SpeakerSpeciality, AvailableSocialMedia diff --git a/apps/events/models/partners.py b/apps/events/models/partners.py index 575b813..5237fd8 100644 --- a/apps/events/models/partners.py +++ b/apps/events/models/partners.py @@ -1,26 +1,26 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ -from apps.users.models import BaseModel - - -class Partner(BaseModel): - name = models.CharField( - max_length=255, unique=True, - help_text=_("The name of the partner"), verbose_name=_("Name") - ) - logo = models.URLField( - help_text=_("The logo of the partner"), verbose_name=_("Logo") - ) - about = models.TextField( - help_text=_("About the partner"), verbose_name=_("About") - ) - website = models.URLField( - help_text=_("The website of the partner"), verbose_name=_("Website") - ) - - def __str__(self): - return self.name - - class Meta: - verbose_name = _("Partner") +from django.db import models +from django.utils.translation import gettext_lazy as _ +from apps.users.models import BaseModel + + +class Partner(BaseModel): + name = models.CharField( + max_length=255, unique=True, + help_text=_("The name of the partner"), verbose_name=_("Name") + ) + logo = models.URLField( + help_text=_("The logo of the partner"), verbose_name=_("Logo") + ) + about = models.TextField( + help_text=_("About the partner"), verbose_name=_("About") + ) + website = models.URLField( + help_text=_("The website of the partner"), verbose_name=_("Website") + ) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Partner") verbose_name_plural = _("Partners") \ No newline at end of file diff --git a/apps/events/models/projects.py b/apps/events/models/projects.py index 3a73b91..16d16d4 100644 --- a/apps/events/models/projects.py +++ b/apps/events/models/projects.py @@ -1,28 +1,28 @@ -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from apps.users.models import BaseModel - - -class Event(BaseModel): - """ - Event model - """ - title = models.CharField(max_length=50, verbose_name=_("Title")) - description = models.TextField(verbose_name=_("Description")) - date = models.DateField(verbose_name=_("Date")) - maintainers = models.ManyToManyField( - "users.User", verbose_name=_("Maintainers"), - help_text=_("Maintainers of the event"), related_name="maintained_events", - ) - github_link = models.URLField( - null=True, blank=True, verbose_name=_("GitHub Link"), - help_text=_("Link to the GitHub repository"), - ) - - class Meta: - db_table = "events" - verbose_name = _("Event") - verbose_name_plural = _("Events") - -# Gallery to be added here later from: https://developers.google.com/photos/library/guides/overview +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from apps.users.models import BaseModel + + +class Event(BaseModel): + """ + Event model + """ + title = models.CharField(max_length=50, verbose_name=_("Title")) + description = models.TextField(verbose_name=_("Description")) + date = models.DateField(verbose_name=_("Date")) + maintainers = models.ManyToManyField( + "users.User", verbose_name=_("Maintainers"), + help_text=_("Maintainers of the event"), related_name="maintained_events", + ) + github_link = models.URLField( + null=True, blank=True, verbose_name=_("GitHub Link"), + help_text=_("Link to the GitHub repository"), + ) + + class Meta: + db_table = "events" + verbose_name = _("Event") + verbose_name_plural = _("Events") + +# Gallery to be added here later from: https://developers.google.com/photos/library/guides/overview diff --git a/apps/events/routes/extra.py b/apps/events/routes/extra.py index f53e2b0..47b3c05 100644 --- a/apps/events/routes/extra.py +++ b/apps/events/routes/extra.py @@ -1,7 +1,7 @@ -from django.urls import path - -from apps.events.views.uploader import FileUploadView - -urlpatterns = [ - path('upload/', FileUploadView.as_view(), name='file-upload'), -] +from django.urls import path + +from apps.events.views.uploader import FileUploadView + +urlpatterns = [ + path('upload/', FileUploadView.as_view(), name='file-upload'), +] diff --git a/apps/events/serializers/upload_serializer.py b/apps/events/serializers/upload_serializer.py index 02d8827..1930a62 100644 --- a/apps/events/serializers/upload_serializer.py +++ b/apps/events/serializers/upload_serializer.py @@ -1,5 +1,5 @@ -from rest_framework import serializers - - -class UploadSerializer(serializers.Serializer): - file = serializers.URLField() +from rest_framework import serializers + + +class UploadSerializer(serializers.Serializer): + file = serializers.URLField() diff --git a/apps/events/signals/main.py b/apps/events/signals/main.py index a1323a4..05e2954 100644 --- a/apps/events/signals/main.py +++ b/apps/events/signals/main.py @@ -1,10 +1,10 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver -from apps.events.models import Speaker, SpeakerSocialMedia - -@receiver(post_save, sender=Speaker) -def update_speaker_social_media_active_status(sender, instance, **kwargs): - social_media_accounts = SpeakerSocialMedia.objects.filter(speaker=instance) - for account in social_media_accounts: - account.active = instance.active +from django.db.models.signals import post_save +from django.dispatch import receiver +from apps.events.models import Speaker, SpeakerSocialMedia + +@receiver(post_save, sender=Speaker) +def update_speaker_social_media_active_status(sender, instance, **kwargs): + social_media_accounts = SpeakerSocialMedia.objects.filter(speaker=instance) + for account in social_media_accounts: + account.active = instance.active account.save() \ No newline at end of file diff --git a/apps/events/views/uploader.py b/apps/events/views/uploader.py index 1b12054..ee6cf69 100644 --- a/apps/events/views/uploader.py +++ b/apps/events/views/uploader.py @@ -1,41 +1,41 @@ -from django.core.files.base import ContentFile -from django.core.files.storage import default_storage -from django.http import JsonResponse -from drf_spectacular.utils import extend_schema, OpenApiResponse -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.views import APIView - -from apps.events.serializers.upload_serializer import UploadSerializer -from mixins import APIResponseMixin -from rest_framework.parsers import MultiPartParser - -class FileUploadView(APIView, APIResponseMixin): - """ - View to handle file uploads. - """ - permission_classes = [IsAuthenticated] - serializer_class = UploadSerializer - parser_classes = [MultiPartParser] - - - @extend_schema( - operation_id="Upload a file", - description="Upload a file to the server.", - tags=["File Upload"], - request=UploadSerializer, - responses={201: OpenApiResponse(description="File uploaded successfully")}, - ) - def post(self, request, *args, **kwargs): - if 'file' not in request.FILES: - return JsonResponse({'error': 'No file provided'}, status=400) - - file = request.FILES['file'] - file_name = default_storage.save(file.name, ContentFile(file.read())) - file_url = default_storage.url(file_name) - - return self.success( - message='File uploaded successfully', - data={'file_url': file_url}, - status_code=status.HTTP_201_CREATED - ) +from django.core.files.base import ContentFile +from django.core.files.storage import default_storage +from django.http import JsonResponse +from drf_spectacular.utils import extend_schema, OpenApiResponse +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView + +from apps.events.serializers.upload_serializer import UploadSerializer +from mixins import APIResponseMixin +from rest_framework.parsers import MultiPartParser + +class FileUploadView(APIView, APIResponseMixin): + """ + View to handle file uploads. + """ + permission_classes = [IsAuthenticated] + serializer_class = UploadSerializer + parser_classes = [MultiPartParser] + + + @extend_schema( + operation_id="Upload a file", + description="Upload a file to the server.", + tags=["File Upload"], + request=UploadSerializer, + responses={201: OpenApiResponse(description="File uploaded successfully")}, + ) + def post(self, request, *args, **kwargs): + if 'file' not in request.FILES: + return JsonResponse({'error': 'No file provided'}, status=400) + + file = request.FILES['file'] + file_name = default_storage.save(file.name, ContentFile(file.read())) + file_url = default_storage.url(file_name) + + return self.success( + message='File uploaded successfully', + data={'file_url': file_url}, + status_code=status.HTTP_201_CREATED + ) diff --git a/apps/users/helpers/auth.py b/apps/users/helpers/auth.py index 4071315..ceaad62 100644 --- a/apps/users/helpers/auth.py +++ b/apps/users/helpers/auth.py @@ -1,29 +1,29 @@ -import secrets -from datetime import timedelta - -from django.contrib.auth import authenticate -from django.utils.timezone import now -from oauth2_provider.models import AccessToken, RefreshToken, Application - - -def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs) - - -def generate_tokens(self, user): - application, _ = Application.objects.get_or_create(name="Default") - expiration_time = now() + timedelta(days=1) - - access_token = AccessToken.objects.create( - user=user, - application=application, - expires=expiration_time, - token=secrets.token_hex(16), - ) - refresh_token = RefreshToken.objects.create( - user=user, - application=application, - token=secrets.token_hex(16), - access_token=access_token, - ) - return {'access_token': access_token, 'refresh_token': refresh_token} +import secrets +from datetime import timedelta + +from django.contrib.auth import authenticate +from django.utils.timezone import now +from oauth2_provider.models import AccessToken, RefreshToken, Application + + +def get_serializer(self, *args, **kwargs): + return self.serializer_class(*args, **kwargs) + + +def generate_tokens(self, user): + application, _ = Application.objects.get_or_create(name="Default") + expiration_time = now() + timedelta(days=1) + + access_token = AccessToken.objects.create( + user=user, + application=application, + expires=expiration_time, + token=secrets.token_hex(16), + ) + refresh_token = RefreshToken.objects.create( + user=user, + application=application, + token=secrets.token_hex(16), + access_token=access_token, + ) + return {'access_token': access_token, 'refresh_token': refresh_token} diff --git a/apps/users/models/base_model.py b/apps/users/models/base_model.py index 3bfff11..0650e39 100644 --- a/apps/users/models/base_model.py +++ b/apps/users/models/base_model.py @@ -1,56 +1,56 @@ -from crequest.middleware import CrequestMiddleware -from django.conf import settings -from django.db import models -from django.utils.translation import gettext_lazy as _ - -from utils.main import generate_uuid - - -class BaseModel(models.Model): - """ - Abstract model that contains common fields for all models, including tracking fields - for creation and modification by users. - """ - id = models.UUIDField( - primary_key=True, - default=generate_uuid, - editable=False, - help_text=_("Unique identifier for this object") - ) - active = models.BooleanField(default=False) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - created_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - related_name="created_%(class)s_set", - editable=False, - null=True, - on_delete=models.SET_NULL - ) - updated_by = models.ForeignKey( - settings.AUTH_USER_MODEL, - related_name="updated_%(class)s_set", - editable=False, - null=True, - on_delete=models.SET_NULL - ) - - def save(self, *args, **kwargs): - try: - request = CrequestMiddleware.get_request() - if request and request.user.is_authenticated: - user = request.user - else: - user = None - except Exception: - user = None - - if not self.created_by_id: - self.created_by = user - - self.updated_by = user - - super().save(*args, **kwargs) - - class Meta: - abstract = True +from crequest.middleware import CrequestMiddleware +from django.conf import settings +from django.db import models +from django.utils.translation import gettext_lazy as _ + +from utils.main import generate_uuid + + +class BaseModel(models.Model): + """ + Abstract model that contains common fields for all models, including tracking fields + for creation and modification by users. + """ + id = models.UUIDField( + primary_key=True, + default=generate_uuid, + editable=False, + help_text=_("Unique identifier for this object") + ) + active = models.BooleanField(default=False) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name="created_%(class)s_set", + editable=False, + null=True, + on_delete=models.SET_NULL + ) + updated_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + related_name="updated_%(class)s_set", + editable=False, + null=True, + on_delete=models.SET_NULL + ) + + def save(self, *args, **kwargs): + try: + request = CrequestMiddleware.get_request() + if request and request.user.is_authenticated: + user = request.user + else: + user = None + except Exception: + user = None + + if not self.created_by_id: + self.created_by = user + + self.updated_by = user + + super().save(*args, **kwargs) + + class Meta: + abstract = True diff --git a/apps/users/pagination.py b/apps/users/pagination.py index d1e5a28..f531a55 100644 --- a/apps/users/pagination.py +++ b/apps/users/pagination.py @@ -1,26 +1,26 @@ -from rest_framework.pagination import PageNumberPagination -from rest_framework.response import Response - - -class CustomPagination(PageNumberPagination): - page_size_query_param = 'page_size' - page_query_param = 'page'.lower() - max_page_size = 100 - - def get_paginated_response(self, data): - return Response({ - 'status': True, - 'message': 'Data retrieved successfully', - 'status_code': 200, - 'page': self.page.number, - 'page_size': self.page.paginator.per_page, - 'total': self.page.paginator.count, - 'pagination': { - 'next': self.get_next_link(), - 'previous': self.get_previous_link(), - 'count': self.page.paginator.count, - 'current_page': self.page.number, - 'total_pages': self.page.paginator.num_pages - }, - 'data': data - }) +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response + + +class CustomPagination(PageNumberPagination): + page_size_query_param = 'page_size' + page_query_param = 'page'.lower() + max_page_size = 100 + + def get_paginated_response(self, data): + return Response({ + 'status': True, + 'message': 'Data retrieved successfully', + 'status_code': 200, + 'page': self.page.number, + 'page_size': self.page.paginator.per_page, + 'total': self.page.paginator.count, + 'pagination': { + 'next': self.get_next_link(), + 'previous': self.get_previous_link(), + 'count': self.page.paginator.count, + 'current_page': self.page.number, + 'total_pages': self.page.paginator.num_pages + }, + 'data': data + }) diff --git a/apps/users/signals/base.py b/apps/users/signals/base.py index 5e04364..75bbc46 100644 --- a/apps/users/signals/base.py +++ b/apps/users/signals/base.py @@ -1,25 +1,25 @@ -from crequest.middleware import CrequestMiddleware -from django.db.models.signals import pre_save -from django.dispatch import receiver - -from apps.users.models import BaseModel - - -@receiver(pre_save, sender=BaseModel) -def update_user_id(sender, instance, **kwargs): - """ - Signal to update the 'updated_by' and 'created_by' fields to the current user whenever a model instance is saved. - """ - try: - request = CrequestMiddleware.get_request() - if request and request.user.is_authenticated: - user = request.user - else: - user = None - except Exception as e: - user = None - - if not instance.created_by_id: - instance.created_by = user - - instance.updated_by = user +from crequest.middleware import CrequestMiddleware +from django.db.models.signals import pre_save +from django.dispatch import receiver + +from apps.users.models import BaseModel + + +@receiver(pre_save, sender=BaseModel) +def update_user_id(sender, instance, **kwargs): + """ + Signal to update the 'updated_by' and 'created_by' fields to the current user whenever a model instance is saved. + """ + try: + request = CrequestMiddleware.get_request() + if request and request.user.is_authenticated: + user = request.user + else: + user = None + except Exception as e: + user = None + + if not instance.created_by_id: + instance.created_by = user + + instance.updated_by = user diff --git a/apps/users/views/general_viewsets.py b/apps/users/views/general_viewsets.py index 0276511..339b911 100644 --- a/apps/users/views/general_viewsets.py +++ b/apps/users/views/general_viewsets.py @@ -1,37 +1,37 @@ -from rest_framework import status -from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet - - -class BaseModelViewSet(ModelViewSet): - """ - A base viewset that you can extend to add custom pagination handling. - """ - - def paginated_response( - self, queryset, request, - serializer_class, message="Success", - status_code=status.HTTP_200_OK, - ): - """ - Custom paginated response method. - """ - page = self.paginate_queryset(queryset) - serializer = serializer_class(page, many=True) - response_data = { - "status": True, - "message": message, - "status_code": status_code, - "page": self.paginator.page.number, - "page_size": self.paginator.page_size, - "total": self.paginator.page.paginator.count, - "pagination": { - "next": self.paginator.get_next_link(), - "previous": self.paginator.get_previous_link(), - "count": self.paginator.page.paginator.count, - "current_page": self.paginator.page.number, - "total_pages": self.paginator.page.paginator.num_pages, - }, - "results": serializer.data - } - return Response(response_data, status=status_code) +from rest_framework import status +from rest_framework.response import Response +from rest_framework.viewsets import ModelViewSet + + +class BaseModelViewSet(ModelViewSet): + """ + A base viewset that you can extend to add custom pagination handling. + """ + + def paginated_response( + self, queryset, request, + serializer_class, message="Success", + status_code=status.HTTP_200_OK, + ): + """ + Custom paginated response method. + """ + page = self.paginate_queryset(queryset) + serializer = serializer_class(page, many=True) + response_data = { + "status": True, + "message": message, + "status_code": status_code, + "page": self.paginator.page.number, + "page_size": self.paginator.page_size, + "total": self.paginator.page.paginator.count, + "pagination": { + "next": self.paginator.get_next_link(), + "previous": self.paginator.get_previous_link(), + "count": self.paginator.page.paginator.count, + "current_page": self.paginator.page.number, + "total_pages": self.paginator.page.paginator.num_pages, + }, + "results": serializer.data + } + return Response(response_data, status=status_code) diff --git a/apps/users/views/index.py b/apps/users/views/index.py index 532daf7..c739e50 100644 --- a/apps/users/views/index.py +++ b/apps/users/views/index.py @@ -1,25 +1,25 @@ -from django.http import JsonResponse - - -def index(request): - data = { - "message": "Looks like we are up and running!", - } - return JsonResponse(data) - -def page_not_found_view(request, exception=None): - return JsonResponse({ - 'status_code': 404, - 'errors': [ - 'The resource was not found' - ] - }) - - -def server_error_view(request): - return JsonResponse({ - 'status_code': 500, - 'errors': [ - 'An error occurred while processing your request', - ] - }) +from django.http import JsonResponse + + +def index(request): + data = { + "message": "Looks like we are up and running!", + } + return JsonResponse(data) + +def page_not_found_view(request, exception=None): + return JsonResponse({ + 'status_code': 404, + 'errors': [ + 'The resource was not found' + ] + }) + + +def server_error_view(request): + return JsonResponse({ + 'status_code': 500, + 'errors': [ + 'An error occurred while processing your request', + ] + }) diff --git a/config/docker/Dockerfile b/config/docker/Dockerfile new file mode 100644 index 0000000..4065772 --- /dev/null +++ b/config/docker/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.11-slim + +RUN apt-get update && apt-get install -y supervisor && rm -rf /var/lib/apt/lists/* + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +RUN pip install --no-cache-dir --upgrade pip==24.2 + +COPY config/docker/requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir -r /app/requirements.txt + +COPY . /app + +RUN python manage.py collectstatic --noinput + +COPY config/docker/supervisord-web.conf /etc/supervisor/conf.d/ +#COPY config/docker/supervisord-celery.conf /etc/supervisor/conf.d/ + +COPY config/script/start.sh /start.sh +RUN chmod +x /start.sh + +EXPOSE 8000 + +CMD ["/start.sh"] diff --git a/config/docker/requirements.txt b/config/docker/requirements.txt new file mode 100644 index 0000000..32f0f93 --- /dev/null +++ b/config/docker/requirements.txt @@ -0,0 +1,25 @@ +beautifulsoup4==4.12.3 +boto3==1.35.0 +cfgv==3.4.0 +celery==5.4.0 +django-cors-headers==4.3.1 +django-crequest==2018.5.11 +django-debug-toolbar==4.4.6 +django-dotenv==1.4.2 +django-extensions==3.2.3 +django-filter==23.5 +django-ninja==1.3.0 +django-oauth-toolkit==2.3.0 +django-storages==1.14.4 +drf-spectacular==0.27.2 +drf-spectacular-sidecar==2024.7.1 +gunicorn==23.0.0 +identify==2.5.33 +nodeenv==1.8.0 +pillow==10.3.0 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 +redis==5.0.8 +tzdata==2023.4 +virtualenv==20.25.0 +whitenoise==6.7.0 \ No newline at end of file diff --git a/config/docker/supervisord-celery.conf b/config/docker/supervisord-celery.conf new file mode 100644 index 0000000..a072506 --- /dev/null +++ b/config/docker/supervisord-celery.conf @@ -0,0 +1,11 @@ +[supervisord] +nodaemon=true +user=root + +[program:celery] +command=/home/django_admin/website_api/venv/bin/celery -A website_api worker -l info +directory=/app +autostart=true +autorestart=true +stderr_logfile=/var/log/celery.err.log +stdout_logfile=/var/log/celery.out.log diff --git a/config/docker/supervisord-web.conf b/config/docker/supervisord-web.conf new file mode 100644 index 0000000..8677d0d --- /dev/null +++ b/config/docker/supervisord-web.conf @@ -0,0 +1,11 @@ +[supervisord] +nodaemon=true +user=root + +[program:web] +command=/home/django_admin/website_api/venv/bin/gunicorn website_api.wsgi:application --bind 0.0.0.0:8000 +directory=/app +autostart=true +autorestart=true +stderr_logfile=/var/log/web.err.log +stdout_logfile=/var/log/web.out.log diff --git a/config/script/start.sh b/config/script/start.sh new file mode 100644 index 0000000..f949249 --- /dev/null +++ b/config/script/start.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord-web.conf & + +#/usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord-celery.conf & + +wait diff --git a/middlewares/translator.py b/middlewares/translator.py index 101b065..c210775 100644 --- a/middlewares/translator.py +++ b/middlewares/translator.py @@ -1,25 +1,25 @@ -from django.utils import translation -from django.utils.deprecation import MiddlewareMixin - - -class APILanguageMiddleware(MiddlewareMixin): - """ - Middleware to set the language for the current request based on the 'Accept-Language' or custom header. - """ - - def process_request(self, request): - lang_code = request.headers.get('Accept-Language') - - if lang_code: - lang_code = lang_code.split(',')[0].strip() - - if lang_code and translation.check_for_language(lang_code): - translation.activate(lang_code) - request.LANGUAGE_CODE = lang_code - else: - translation.activate(translation.get_language()) - - def process_response(self, request, response): - response['Content-Language'] = translation.get_language() - translation.deactivate() - return response +from django.utils import translation +from django.utils.deprecation import MiddlewareMixin + + +class APILanguageMiddleware(MiddlewareMixin): + """ + Middleware to set the language for the current request based on the 'Accept-Language' or custom header. + """ + + def process_request(self, request): + lang_code = request.headers.get('Accept-Language') + + if lang_code: + lang_code = lang_code.split(',')[0].strip() + + if lang_code and translation.check_for_language(lang_code): + translation.activate(lang_code) + request.LANGUAGE_CODE = lang_code + else: + translation.activate(translation.get_language()) + + def process_response(self, request, response): + response['Content-Language'] = translation.get_language() + translation.deactivate() + return response diff --git a/templates/docs/redoc.html b/templates/docs/redoc.html index 3181500..0179133 100644 --- a/templates/docs/redoc.html +++ b/templates/docs/redoc.html @@ -1,26 +1,26 @@ - - -
- - -