From 505ef9215bca92d129b7b88a054bff9633ed8f6a Mon Sep 17 00:00:00 2001 From: Tomilola-ng Date: Sun, 20 Oct 2024 01:12:43 +0100 Subject: [PATCH] First Version Tested and Complete --- .env.sample | 9 ++ .github/workflows/build-image.yaml | 35 ++++++ Dockerfile | 12 ++ accounts/__init__.py | 0 accounts/admin.py | 79 ++++++++++++ accounts/apps.py | 6 + accounts/authentication.py | 24 ++++ accounts/forms.py | 23 ++++ accounts/managers.py | 33 +++++ accounts/migrations/0001_initial.py | 52 ++++++++ accounts/migrations/__init__.py | 0 accounts/models.py | 48 ++++++++ accounts/serializers.py | 68 ++++++++++ accounts/tests.py | 3 + accounts/urls.py | 7 ++ accounts/views.py | 54 ++++++++ config/__init__.py | 0 config/asgi.py | 16 +++ config/settings.py | 184 ++++++++++++++++++++++++++++ config/swagger.py | 25 ++++ config/urls.py | 34 +++++ config/wsgi.py | 16 +++ docker-compose.yml | 13 ++ helpers/__init__.py | 0 helpers/admin.py | 6 + helpers/apps.py | 6 + helpers/models.py | 58 +++++++++ helpers/serilaizers.py | 11 ++ helpers/views.py | 11 ++ manage.py | 22 ++++ requirements.txt | 34 +++++ 31 files changed, 889 insertions(+) create mode 100644 .env.sample create mode 100644 .github/workflows/build-image.yaml create mode 100644 Dockerfile create mode 100644 accounts/__init__.py create mode 100644 accounts/admin.py create mode 100644 accounts/apps.py create mode 100644 accounts/authentication.py create mode 100644 accounts/forms.py create mode 100644 accounts/managers.py create mode 100644 accounts/migrations/0001_initial.py create mode 100644 accounts/migrations/__init__.py create mode 100644 accounts/models.py create mode 100644 accounts/serializers.py create mode 100644 accounts/tests.py create mode 100644 accounts/urls.py create mode 100644 accounts/views.py create mode 100644 config/__init__.py create mode 100644 config/asgi.py create mode 100644 config/settings.py create mode 100644 config/swagger.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100644 docker-compose.yml create mode 100644 helpers/__init__.py create mode 100644 helpers/admin.py create mode 100644 helpers/apps.py create mode 100644 helpers/models.py create mode 100644 helpers/serilaizers.py create mode 100644 helpers/views.py create mode 100644 manage.py create mode 100644 requirements.txt diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..dde3d8f --- /dev/null +++ b/.env.sample @@ -0,0 +1,9 @@ +DEBUG=True +SECRET_KEY="add-your-secret-key-here" +ALLOWED_HOSTS=*,127.0.0.1,localhost + +DATABASE_ENGINE="django.db.backends.sqlite3" +DATABASE_NAME="db.sqlite3" + +CORS_ORIGIN_ALLOW_ALL=True +EMAIL_SENDER="add-your-email-here" diff --git a/.github/workflows/build-image.yaml b/.github/workflows/build-image.yaml new file mode 100644 index 0000000..eea211f --- /dev/null +++ b/.github/workflows/build-image.yaml @@ -0,0 +1,35 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Build Docker image + run: | + docker-compose -f docker-compose.yml build + + - name: Push Docker image + run: | + docker tag image-name:latest yourusername/image-name:latest + docker push yourusername/image-name:latest diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..53216ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.11.6-slim-bullseye + +ENV PYTHONUNBUFFERED=1 + +WORKDIR /django + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY . . + +CMD python3 manage.py runserver 0.0.0.0:8000 \ No newline at end of file diff --git a/accounts/__init__.py b/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/admin.py b/accounts/admin.py new file mode 100644 index 0000000..269fdac --- /dev/null +++ b/accounts/admin.py @@ -0,0 +1,79 @@ +""" User Account Admin """ + +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.utils.translation import gettext_lazy as _ + +from .forms import CustomUserChangeForm, CustomUserCreationForm +from .models import UserAccount + + +class UserAdmin(BaseUserAdmin): + """Define a model for UserAccount.""" + ordering = ["email"] + add_form = CustomUserCreationForm + form = CustomUserChangeForm + model = UserAccount + list_display = [ + "pkid", + "id", + "email", + "first_name", + "last_name", + "is_staff", + "is_active", + ] + list_display_links = ["id", "email"] + list_filter = [ + "email", + "first_name", + "last_name", + "is_staff", + "is_active", + ] + fieldsets = ( + ( + _("Login Credentials"), + { + "fields": ( + "email", + "password", + ) + }, + ), + ( + _("Personal Information"), + { + "fields": ( + "first_name", + "last_name", + ) + }, + ), + ( + _("Permissions and Groups"), + { + "fields": ( + "is_active", + "is_staff", + "is_superuser", + "groups", + "user_permissions", + ) + }, + ), + (_("Important Dates"), {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("email", "password1", "password2", "is_staff", "is_active"), + }, + ), + ) + search_fields = ["email", "first_name", "last_name"] + + +admin.site.register(UserAccount, UserAdmin) diff --git a/accounts/apps.py b/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/accounts/authentication.py b/accounts/authentication.py new file mode 100644 index 0000000..ad9ac22 --- /dev/null +++ b/accounts/authentication.py @@ -0,0 +1,24 @@ +""" Custom Authentication """ + +from rest_framework_simplejwt.authentication import JWTAuthentication +from django.conf import settings + + +class CustomJWTAuthentication(JWTAuthentication): + """Define a custom JWT Authentication.""" + + def authenticate(self, request): + """Authenticate the request.""" + try: + header = self.get_header(request) + if header is None: + raw_token = request.COOKIES.get(settings.AUTH_COOKIE) + else: + raw_token = self.get_raw_token(header) + + if raw_token is None: + return None + validated_token = self.get_validated_token(raw_token) + return self.get_user(validated_token), validated_token + except Exception as e: # pylint: disable=broad-except disable=unused-variable + return None diff --git a/accounts/forms.py b/accounts/forms.py new file mode 100644 index 0000000..99a030b --- /dev/null +++ b/accounts/forms.py @@ -0,0 +1,23 @@ +""" User Account Forms """ + +from django.contrib.auth.forms import UserChangeForm, UserCreationForm + +from .models import UserAccount + + +class CustomUserCreationForm(UserCreationForm): + """Define a form for creating UserAccount.""" + class Meta(UserCreationForm): + """Define Meta options for CustomUserCreationForm.""" + model = UserAccount + fields = ["email", "first_name", "last_name"] + error_class = "error" + + +class CustomUserChangeForm(UserChangeForm): + """Define a form for updating UserAccount.""" + class Meta: + """Define Meta options for CustomUserChangeForm.""" + model = UserAccount + fields = ["email", "first_name", "last_name"] + error_class = "error" diff --git a/accounts/managers.py b/accounts/managers.py new file mode 100644 index 0000000..d693dc1 --- /dev/null +++ b/accounts/managers.py @@ -0,0 +1,33 @@ +""" User Account Manager """ + +from django.contrib.auth.models import BaseUserManager + + +class UserAccountManager(BaseUserManager): + """Define a model manager for UserAccount.""" + + def create_user(self, email, password=None, **extra_fields): + """Create and save a User with the given email and password.""" + if not email: + raise ValueError('Users must have an email address') + + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + user.set_password(password) + user.save() + return user + + def create_superuser(self, email, password=None, **kwargs): + """Create and save a SuperUser with the given email and password.""" + kwargs.setdefault('is_active', True) + kwargs.setdefault('is_staff', True) + kwargs.setdefault('is_superuser', True) + + if kwargs.get('is_active') is not True: + raise ValueError('Super User must be active') + if kwargs.get('is_staff') is not True: + raise ValueError('Super User must be staff') + if kwargs.get('is_superuser') is not True: + raise ValueError('Super User must have is superuser is True') + + return self.create_user(email, password, **kwargs) diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..a0a94d5 --- /dev/null +++ b/accounts/migrations/0001_initial.py @@ -0,0 +1,52 @@ +""" Django Migrations """ +# pylint: disable=line-too-long disable=invalid-name + +import uuid +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + """ Migration """ + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='UserAccount', + fields=[ + ('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')), + ('pkid', models.BigAutoField( + editable=False, primary_key=True, serialize=False)), + ('id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('email', models.EmailField(db_index=True, max_length=255, + unique=True, verbose_name='Email Address')), + ('role', models.CharField(choices=[ + ('default', 'Default'), ('admin', 'Admin')], db_index=True, default='default', max_length=20)), + ('first_name', models.CharField( + max_length=255, verbose_name='First Name')), + ('last_name', models.CharField( + max_length=255, verbose_name='Last Name')), + ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False)), + ('date_joined', models.DateTimeField( + default=django.utils.timezone.now)), + ('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')), + ], + options={ + 'verbose_name': 'User', + 'verbose_name_plural': 'Users', + }, + ), + ] diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/accounts/models.py b/accounts/models.py new file mode 100644 index 0000000..6178f51 --- /dev/null +++ b/accounts/models.py @@ -0,0 +1,48 @@ +""" User Account Model """ + +import uuid + +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.db import models +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from accounts.managers import UserAccountManager + + +class UserAccount(AbstractBaseUser, PermissionsMixin): + """Define a model for UserAccount.""" + + ROLE_CHOICES = ( + ('default', 'Default'), + ('admin', 'Admin'), + ) + + pkid = models.BigAutoField(primary_key=True, editable=False) + id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True) + email = models.EmailField(verbose_name=_( + "Email Address"), max_length=255, unique=True, db_index=True) + role = models.CharField(max_length=20, choices=ROLE_CHOICES, + default='default', db_index=True) + first_name = models.CharField(verbose_name=_("First Name"), max_length=255) + last_name = models.CharField(verbose_name=_("Last Name"), max_length=255) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + date_joined = models.DateTimeField(default=timezone.now) + + objects = UserAccountManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['first_name', 'last_name'] + + class Meta: + """Define Meta options for UserAccount.""" + verbose_name = _("User") + verbose_name_plural = _("Users") + + def get_short_name(self): + """ Return the short name for the user. """ + return self.first_name + + def __str__(self): + return f"{self.email}" diff --git a/accounts/serializers.py b/accounts/serializers.py new file mode 100644 index 0000000..cf60898 --- /dev/null +++ b/accounts/serializers.py @@ -0,0 +1,68 @@ +""" User Account Serializers """ + +from django.contrib.auth import get_user_model +from djoser.serializers import UserCreateSerializer +from rest_framework import serializers +from rest_framework_simplejwt.serializers import TokenObtainPairSerializer + +User = get_user_model() + + +class UserSerializer(serializers.ModelSerializer): + """Define a serializer for UserAccount.""" + first_name = serializers.SerializerMethodField() + last_name = serializers.SerializerMethodField() + + class Meta: + """Define Meta options for UserSerializer.""" + model = User + fields = [ + "id", + "email", + "first_name", + "last_name", + ] + + def get_first_name(self, obj): + """Return the first name of the user.""" + return obj.first_name.title() + + def get_last_name(self, obj): + """Return the last name of the user.""" + return obj.last_name.title() + + def to_representation(self, instance): + """Return the representation of the user.""" + representation = super( + UserSerializer, self).to_representation(instance) + if instance.is_superuser: + representation["admin"] = True + return representation + + +class CreateUserSerializer(UserCreateSerializer): + """Define a serializer for creating UserAccount.""" + class Meta(UserCreateSerializer.Meta): + """Define Meta options for CreateUserSerializer.""" + model = User + fields = ["id", "email", "first_name", "last_name", "password"] + + +class LoginUserSerializer(TokenObtainPairSerializer): # pylint: disable=abstract-method + """Define a serializer for login UserAccount.""" + + def validate(self, attrs): + """Validate the user credentials.""" + data = super().validate(attrs) + user = self.user + data.update({ + 'user': { + 'id': user.id, + 'email': user.email, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'role': user.role, + } + }) + + return data diff --git a/accounts/tests.py b/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/accounts/urls.py b/accounts/urls.py new file mode 100644 index 0000000..346c675 --- /dev/null +++ b/accounts/urls.py @@ -0,0 +1,7 @@ +""" User Account URLs """ + +from django.urls import include, path + +urlpatterns = [ + path('auth/', include('djoser.urls')), +] diff --git a/accounts/views.py b/accounts/views.py new file mode 100644 index 0000000..78c4e64 --- /dev/null +++ b/accounts/views.py @@ -0,0 +1,54 @@ +""" User Account Views """ + +from rest_framework import generics +from rest_framework_simplejwt.views import TokenObtainPairView + +from django.conf import settings + +from accounts.models import UserAccount +from accounts.serializers import LoginUserSerializer, CreateUserSerializer + + +class LoginUserView(TokenObtainPairView): + """ + Login User Endpoint + - Requires Email and Password + - returns JWT Tokens and User Data + """ + serializer_class = LoginUserSerializer + + def post(self, request, *args, **kwargs): + """ Post Request """ + data = super().post(request, *args, **kwargs) + + if data.status_code == 200: + access_token = data.data.get('access') + refresh_token = data.data.get('refresh') + + data.set_cookie( + 'access', + access_token, + max_age=settings.AUTH_COOKIE_MAX_AGE, + path=settings.AUTH_COOKIE_PATH, + secure=settings.AUTH_COOKIE_SECURE, + httponly=settings.AUTH_COOKIE_HTTP_ONLY, + samesite=settings.AUTH_COOKIE_SAMESITE + ) + data.set_cookie( + 'refresh', + refresh_token, + max_age=settings.AUTH_COOKIE_MAX_AGE, + path=settings.AUTH_COOKIE_PATH, + secure=settings.AUTH_COOKIE_SECURE, + httponly=settings.AUTH_COOKIE_HTTP_ONLY, + samesite=settings.AUTH_COOKIE_SAMESITE + ) + return data + + +class RegisterUserView(generics.CreateAPIView): + """ + Create a new User Endpoint + """ + queryset = UserAccount.objects.all() + serializer_class = CreateUserSerializer diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..400e9e3 --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..e395272 --- /dev/null +++ b/config/settings.py @@ -0,0 +1,184 @@ +""" + This is the settings file for the Django project. + It is used to configure the project and its components. + - Created by: Tomilola Oluwafemi +""" + +from pathlib import Path +from datetime import timedelta + +import environ + +env = environ.Env() +environ.Env.read_env() + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +SECRET_KEY = env('SECRET_KEY') + +DEBUG = env('DEBUG') + +ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=[]) + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + + # Package Apps + 'djoser', + 'drf_yasg', + 'corsheaders', + 'rest_framework', + + # Custom Apps + 'accounts.apps.AccountsConfig', + 'helpers.apps.HelpersConfig', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'corsheaders.middleware.CorsMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'config.wsgi.application' + +DATABASES = { + 'default': { + 'ENGINE': env('DATABASE_ENGINE'), + 'NAME': BASE_DIR / env('DATABASE_NAME'), + } +} + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_TZ = True + +STATIC_URL = 'static/' +MEDIA_URL = 'media/' + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +""" + Django Rest Framework Settings +""" + +REST_FRAMEWORK = { + 'DEFAULT_PERMISSION_CLASSES': [ + 'rest_framework.permissions.IsAuthenticatedOrReadOnly', + ], + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'accounts.authentication.CustomJWTAuthentication', + ), + 'DEFAULT_FILTER_BACKENDS': ( + 'django_filters.rest_framework.DjangoFilterBackend', + 'rest_framework.filters.OrderingFilter' + ), + 'DEFAULT_PAGINATION_CLASS': "rest_framework.pagination.PageNumberPagination", + 'PAGE_SIZE': 20 +} + + +CORS_ALLOW_ALL_ORIGINS = env.bool('CORS_ORIGIN_ALLOW_ALL', default=False) + +SIMPLE_JWT = { + "AUTH_HEADER_TYPES": ( + "Bearer", + "JWT", + ), + 'ACCESS_TOKEN_LIFETIME': timedelta(days=3), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=21), + "AUTH_HEADER_NAME": "HTTP_AUTHORIZATION", + "AUTH_TOKEN_CLASSES": ("rest_framework_simplejwt.tokens.AccessToken",), +} + + +AUTHENTICATION_BACKENDS = ( + 'django.contrib.auth.backends.ModelBackend', +) + +AUTH_COOKIE = 'access' +AUTH_COOKIE_MAX_AGE = timedelta(days=21) +AUTH_COOKIE_SECURE = True +AUTH_COOKIE_HTTP_ONLY = True +AUTH_COOKIE_PATH = '/' +AUTH_COOKIE_SAMESITE = 'None' + +DJOSER = { + 'LOGIN_FIELD': 'email', + 'USER_CREATE_PASSWORD_RETYPE': True, + 'USERNAME_CHANGED_EMAIL_CONFIRMATION': True, + 'PASSWORD_CHANGED_EMAIL_CONFIRMATION': True, + 'SEND_CONFIRMATION_EMAIL': True, + 'SET_PASSWORD_RETYPE': True, + 'PASSWORD_RESET_CONFIRM_URL': 'password/reset/confirm/{uid}/{token}', + 'USERNAME_RESET_CONFIRM_URL': 'email/reset/confirm/{uid}/{token}', + 'ACTIVATION_URL': '/activate/{uid}/{token}', + 'SEND_ACTIVATION_EMAIL': False, + 'SERIALIZERS': { + "user_create": "accounts.serializers.CreateUserSerializer,", + "user": "accounts.serializers.UserSerializer", + "current_user": "accounts.serializers.UserSerializer", + "user_delete": "djoser.serializers.UserDeleteSerializer", + } +} + + +AUTH_USER_MODEL = 'accounts.UserAccount' + +""" + Environment Variables +""" +EMAIL_SENDER = env('EMAIL_SENDER') diff --git a/config/swagger.py b/config/swagger.py new file mode 100644 index 0000000..7440db0 --- /dev/null +++ b/config/swagger.py @@ -0,0 +1,25 @@ +""" Swagger Settings """ + +from django.urls import path +from django.conf import settings + +from drf_yasg import openapi +from drf_yasg.views import get_schema_view + + +SchemaView = get_schema_view( + openapi.Info( + title="Django DRF API", + default_version='v1', + description="API documentation Template for Django DRF, Created by: Tomilola Oluwafemi", + contact=openapi.Contact(email=settings.EMAIL_SENDER), + ), + public=True, +) + +urlpatterns = [ + path('', SchemaView.with_ui('swagger', cache_timeout=0), + name='schema-swagger-ui'), + path('redoc/', SchemaView.with_ui('redoc', + cache_timeout=0), name='schema-redoc'), +] diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..9505c1b --- /dev/null +++ b/config/urls.py @@ -0,0 +1,34 @@ +""" + This is the URL Configuration file for the Django project. + It is used to configure the project and its components. + - Created by: Tomilola Oluwafemi +""" + +from django.contrib import admin +from django.urls import path, include + +from rest_framework import routers + +from helpers.views import ImageAssetViewSet +from accounts.views import LoginUserView, RegisterUserView +# Import more API Views here +from config.swagger import urlpatterns as swagger + + +router = routers.DefaultRouter() + +# Register more API views here +router.register(r'image-assets', ImageAssetViewSet, basename='image-assets') + +urlpatterns = [ + path('admin/', admin.site.urls), + path("api/v1/", include(router.urls)), + path('api/v1/accounts/', include('accounts.urls')), + + path('api/v1/auth/login/', LoginUserView.as_view(), name='login-user'), + path('api/v1/auth/register/', RegisterUserView.as_view(), name='register-user'), + + # API DOCUMENTAION + path('', include(swagger)), + path('api/', include(swagger)), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..e232e56 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..81023b7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.11" +services: + app: + build: . + volumes: + - .:/django + ports: + - "8000:8000" + image: yourusername/image-name:latest + container_name: container-name + command: python3 manage.py runserver 0.0.0.0:8000 + env_file: + - config/.env diff --git a/helpers/__init__.py b/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helpers/admin.py b/helpers/admin.py new file mode 100644 index 0000000..e73cdfc --- /dev/null +++ b/helpers/admin.py @@ -0,0 +1,6 @@ +"""Admin configuration""" + +from django.contrib import admin +from helpers.models import ImageAsset + +admin.site.register(ImageAsset) diff --git a/helpers/apps.py b/helpers/apps.py new file mode 100644 index 0000000..8b4c77e --- /dev/null +++ b/helpers/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class HelpersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "helpers" diff --git a/helpers/models.py b/helpers/models.py new file mode 100644 index 0000000..1c60227 --- /dev/null +++ b/helpers/models.py @@ -0,0 +1,58 @@ +"""Helper models""" + +from django.db import models + + +class PageModel(models.Model): + """Base model for all models""" + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + meta_title = models.CharField(max_length=255, blank=True, null=True) + meta_description = models.CharField(max_length=255, blank=True, null=True) + meta_keywords = models.CharField(max_length=255, blank=True, null=True) + slug = models.ForeignKey( + 'SuperSlug', on_delete=models.CASCADE, related_name='page_slugs') + + objects = models.Manager() + + class Meta: + """Meta options""" + abstract = True + ordering = ["-updated_at"] + verbose_name = "Page" + verbose_name_plural = "Pages" + + def __str__(self) -> str: + return f"{self.slug}" + + +class ImageAsset(models.Model): + """Image Asset model""" + image = models.ImageField(upload_to="image-assets/") + alt = models.CharField(max_length=255, blank=True, null=True) + + objects = models.Manager() + + class Meta: + """Meta options""" + verbose_name = "Image Asset" + verbose_name_plural = "Image Assets" + + def __str__(self) -> str: + return f"{self.image}" + + +class SuperSlug(models.Model): + """Super Slug model""" + slug = models.SlugField(unique=True) + + objects = models.Manager() + + class Meta: + """Meta options""" + abstract = True + verbose_name = "Super Slug" + verbose_name_plural = "Super Slugs" + + def __str__(self) -> str: + return f"{self.slug}" diff --git a/helpers/serilaizers.py b/helpers/serilaizers.py new file mode 100644 index 0000000..884f209 --- /dev/null +++ b/helpers/serilaizers.py @@ -0,0 +1,11 @@ +"""Helper serializers""" + +from rest_framework import serializers +from helpers.models import ImageAsset + +class ImageAssetSerializer(serializers.ModelSerializer): + """Image Asset serializer""" + class Meta: + """Meta options""" + model = ImageAsset + fields = "__all__" diff --git a/helpers/views.py b/helpers/views.py new file mode 100644 index 0000000..85deb78 --- /dev/null +++ b/helpers/views.py @@ -0,0 +1,11 @@ +"""Helper views""" + +from rest_framework import viewsets +from helpers.models import ImageAsset +from helpers.serilaizers import ImageAssetSerializer + + +class ImageAssetViewSet(viewsets.ModelViewSet): + """Image Asset view set""" + queryset = ImageAsset.objects.all() + serializer_class = ImageAssetSerializer diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..8e7ac79 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7635b7e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,34 @@ +asgiref==3.8.1 +certifi==2024.8.30 +cffi==1.17.1 +charset-normalizer==3.4.0 +cryptography==43.0.3 +defusedxml==0.8.0rc2 +Django==5.1.2 +django-cors-headers==4.5.0 +django-environ==0.11.2 +django-templated-mail==1.1.1 +djangorestframework==3.15.2 +djangorestframework-simplejwt==5.3.1 +djoser==2.2.3 +drf-yasg==1.21.8 +gunicorn==23.0.0 +idna==3.10 +inflection==0.5.1 +oauthlib==3.2.2 +packaging==24.1 +pillow==11.0.0 +pycparser==2.22 +PyJWT==2.9.0 +python3-openid==3.2.0 +pytz==2024.2 +PyYAML==6.0.2 +requests==2.32.3 +requests-oauthlib==2.0.0 +social-auth-app-django==5.4.2 +social-auth-core==4.5.4 +sqlparse==0.5.1 +tzdata==2024.2 +uritemplate==4.1.1 +urllib3==2.2.3 +whitenoise==6.7.0