Django + Next + Postgres + Docker + Nginx
need multiple monitors now
23 files changed, +355 -3
+0-0backend/api/__init__.py
No content changes (mode or rename only).
+3-0backend/api/admin.py
| @@ -0,0 +1,3 @@ | ||
| 1 | +from django.contrib import admin | |
| 2 | + | |
| 3 | +# Register your models here. |
+6-0backend/api/apps.py
| @@ -0,0 +1,6 @@ | ||
| 1 | +from django.apps import AppConfig | |
| 2 | + | |
| 3 | + | |
| 4 | +class ApiConfig(AppConfig): | |
| 5 | + default_auto_field = 'django.db.models.BigAutoField' | |
| 6 | + name = 'api' |
+0-0backend/api/auth/__init__.py
No content changes (mode or rename only).
+41-0backend/api/auth/serializers.py
| @@ -0,0 +1,41 @@ | ||
| 1 | +from rest_framework import serializers | |
| 2 | +from django.contrib.auth.models import User | |
| 3 | +from rest_framework.exceptions import ValidationError, NotFound, AuthenticationFailed | |
| 4 | +from django.contrib.auth import login, logout, authenticate | |
| 5 | + | |
| 6 | + | |
| 7 | +class LoginSerializer(serializers.ModelSerializer): | |
| 8 | + username = serializers.CharField(required=False) | |
| 9 | + email = serializers.EmailField(required=False) | |
| 10 | + password = serializers.CharField(write_only=True) | |
| 11 | + class Meta: | |
| 12 | + model = User | |
| 13 | + fields = ['username', 'email', 'password'] | |
| 14 | + | |
| 15 | + def create(self, validated_data): | |
| 16 | + request = self.context['request'] | |
| 17 | + logout(request) | |
| 18 | + username = validated_data.get('username') | |
| 19 | + email = validated_data.get('email') | |
| 20 | + password = validated_data.get('password') | |
| 21 | + if not username and not email: | |
| 22 | + raise ValidationError('Requires username or email.') | |
| 23 | + if username: | |
| 24 | + user = authenticate(username=username, password=password) | |
| 25 | + else: | |
| 26 | + query = User.objects.filter(email=email) | |
| 27 | + if not query: | |
| 28 | + raise NotFound(f'{email} has no account.') | |
| 29 | + username = query.first().username | |
| 30 | + user = authenticate(username, password) | |
| 31 | + if not user: | |
| 32 | + raise AuthenticationFailed('Incorrect username or password.') | |
| 33 | + login(self.context['request'], user) | |
| 34 | + return user | |
| 35 | + | |
| 36 | + | |
| 37 | +class LogoutSerializer(serializers.Serializer): | |
| 38 | + detail = serializers.CharField(read_only=True, default="Success") | |
| 39 | + | |
| 40 | +class LoggedInSerializer(serializers.Serializer): | |
| 41 | + detail = serializers.BooleanField(read_only=True, default=False) |
+11-0backend/api/auth/urls.py
| @@ -0,0 +1,11 @@ | ||
| 1 | +from django.urls import path | |
| 2 | +from .views import LoginView, AuthRoot, LogoutView, LoggedInView | |
| 3 | + | |
| 4 | + | |
| 5 | +urlpatterns = [ | |
| 6 | + path('login', LoginView.as_view(), name='api-login'), | |
| 7 | + path('', AuthRoot.as_view(), name='api-auth-root'), | |
| 8 | + path('logout', LogoutView.as_view(), name='api-logout'), | |
| 9 | + path('logged-in', LoggedInView.as_view(), name='api-logged-in') | |
| 10 | +] | |
| 11 | + |
+66-0backend/api/auth/views.py
| @@ -0,0 +1,66 @@ | ||
| 1 | +from rest_framework.generics import CreateAPIView, GenericAPIView, RetrieveAPIView | |
| 2 | +from rest_framework.views import APIView | |
| 3 | +from rest_framework.routers import APIRootView | |
| 4 | +from rest_framework.reverse import reverse | |
| 5 | +from rest_framework.response import Response | |
| 6 | +from rest_framework.permissions import IsAuthenticated | |
| 7 | +from rest_framework.schemas.openapi import AutoSchema | |
| 8 | +from rest_framework.permissions import AllowAny | |
| 9 | +from .serializers import LoginSerializer, LogoutSerializer, LoggedInSerializer | |
| 10 | +from django.contrib.auth.models import User | |
| 11 | +from django.contrib.auth import logout | |
| 12 | + | |
| 13 | + | |
| 14 | +class AuthRoot(APIRootView): | |
| 15 | + | |
| 16 | + def get(self, request, *args, **kwargs): | |
| 17 | + return Response({ | |
| 18 | + 'login': reverse('api-login', request=request), | |
| 19 | + 'logout': reverse('api-logout', request=request), | |
| 20 | + 'logged-in': reverse('api-logged-in', request=request) | |
| 21 | + }) | |
| 22 | + | |
| 23 | + | |
| 24 | +class LoginView(CreateAPIView): | |
| 25 | + schema = AutoSchema( | |
| 26 | + tags=['Auth'], | |
| 27 | + component_name='login', | |
| 28 | + operation_id_base='Login' | |
| 29 | + ) | |
| 30 | + serializer_class = LoginSerializer | |
| 31 | + queryset = User.objects.all() | |
| 32 | + permission_classes = [AllowAny] | |
| 33 | + | |
| 34 | + | |
| 35 | +class LoggedInView(RetrieveAPIView): | |
| 36 | + schema = AutoSchema( | |
| 37 | + tags=['Auth'], | |
| 38 | + component_name='logged-in', | |
| 39 | + operation_id_base='LoggedIn' | |
| 40 | + ) | |
| 41 | + serializer_class = LoggedInSerializer | |
| 42 | + | |
| 43 | + def retrieve(self, request, *args, **kwargs): | |
| 44 | + if request.user.is_authenticated: | |
| 45 | + return Response({ | |
| 46 | + 'detail': True | |
| 47 | + }) | |
| 48 | + return Response({'detail': False}) | |
| 49 | + | |
| 50 | + | |
| 51 | +class LogoutView(RetrieveAPIView): | |
| 52 | + schema = AutoSchema( | |
| 53 | + tags=['Auth'], | |
| 54 | + component_name='logout', | |
| 55 | + operation_id_base='Logout' | |
| 56 | + ) | |
| 57 | + serializer_class = LogoutSerializer | |
| 58 | + permission_classes = [IsAuthenticated] | |
| 59 | + | |
| 60 | + def retrieve(self, request): | |
| 61 | + user = request.user | |
| 62 | + logout(request) | |
| 63 | + return Response({ | |
| 64 | + 'details': f'Successfully logged out {user}.' | |
| 65 | + }) | |
| 66 | + |
+0-0backend/api/management/commands/__init__.py
No content changes (mode or rename only).
+74-0backend/api/management/commands/createapi.py
| @@ -0,0 +1,74 @@ | ||
| 1 | +from django.core.management.base import BaseCommand, CommandError, CommandParser | |
| 2 | +from django.template.loader import render_to_string | |
| 3 | +from django.utils.html import strip_tags | |
| 4 | +import os | |
| 5 | + | |
| 6 | + | |
| 7 | +class Command(BaseCommand): | |
| 8 | + # Show this when the user types help | |
| 9 | + help = "Creates a new API. Usage: python manage.py createapi api_name [root_dir]" | |
| 10 | + | |
| 11 | + def add_arguments(self, parser: CommandParser) -> None: | |
| 12 | + parser.add_argument('model_name', type=str, help='The name of the API model') | |
| 13 | + parser.add_argument('root_dir', type=str, nargs='?', default='api', help='The root directory for the API model. Defaults to "api"') | |
| 14 | + | |
| 15 | + def handle(self, *args, **options): | |
| 16 | + # Get the api name from the first argument | |
| 17 | + model_name = options['model_name'] | |
| 18 | + api_name = model_name.lower() | |
| 19 | + # Get the root directory | |
| 20 | + root_dir = options['root_dir'].lower() | |
| 21 | + # Create context for the templates | |
| 22 | + context = { | |
| 23 | + 'api_name': api_name, | |
| 24 | + 'root_dir': root_dir, | |
| 25 | + 'model_name': model_name, | |
| 26 | + } | |
| 27 | + # If the api model does not exist raise a CommandError | |
| 28 | + try: | |
| 29 | + # Test if the model exists | |
| 30 | + getattr(__import__(root_dir).models, model_name) | |
| 31 | + except: | |
| 32 | + raise CommandError(f"Model {model_name} does not exist") | |
| 33 | + # If the root directory does not exist raise a CommandError | |
| 34 | + if not os.path.exists(root_dir): | |
| 35 | + raise CommandError(f"Directory {root_dir} does not exist") | |
| 36 | + # Create the api directory | |
| 37 | + api_dir = os.path.join(root_dir, api_name) | |
| 38 | + os.makedirs(api_dir, exist_ok=True) | |
| 39 | + # Create an __init__.py file | |
| 40 | + init_file = os.path.join(api_dir, '__init__.py') | |
| 41 | + with open(init_file, 'w') as f: | |
| 42 | + f.write('') | |
| 43 | + # Load the viewset template and write it to the views file | |
| 44 | + views_file = os.path.join(api_dir, 'views.py') | |
| 45 | + with open(views_file, 'w') as f: | |
| 46 | + f.write(strip_tags(render_to_string('api/viewset.html', context))) | |
| 47 | + # Load the serializers template and write it to the serializers file | |
| 48 | + serializers_file = os.path.join(api_dir, 'serializers.py') | |
| 49 | + with open(serializers_file, 'w') as f: | |
| 50 | + f.write(strip_tags(render_to_string('api/serializers.html', context))) | |
| 51 | + # Load the urls template and write it to the urls file | |
| 52 | + urls_file = os.path.join(api_dir, 'urls.py') | |
| 53 | + with open(urls_file, 'w') as f: | |
| 54 | + f.write(strip_tags(render_to_string('api/urls.html', context))) | |
| 55 | + # Add the urls to the root urls file | |
| 56 | + root_urls_file = os.path.join(root_dir, 'urls.py') | |
| 57 | + with open(root_urls_file, 'r') as f: | |
| 58 | + lines = f.readlines() | |
| 59 | + with open(root_urls_file, 'w') as f: | |
| 60 | + for line in lines: | |
| 61 | + f.write(line) | |
| 62 | + if 'Auto-generated' in line: | |
| 63 | + f.write(f"\tre_path('', include('{root_dir}.{api_name}.urls')),\n") | |
| 64 | + # Add the url to the api root | |
| 65 | + api_root_file = os.path.join(root_dir, 'views.py') | |
| 66 | + with open(api_root_file, 'r') as f: | |
| 67 | + lines = f.readlines() | |
| 68 | + with open(api_root_file, 'w') as f: | |
| 69 | + for line in lines: | |
| 70 | + f.write(line) | |
| 71 | + if 'Auto-generated' in line: | |
| 72 | + print('found user-list') | |
| 73 | + f.write(f"\t\t\t'{api_name}': reverse('{api_name}-list', request=request),\n") | |
| 74 | + self.stdout.write(self.style.SUCCESS(f"Successfully created API {api_name}")) |
+3-0backend/api/models.py
| @@ -0,0 +1,3 @@ | ||
| 1 | +from django.db import models | |
| 2 | + | |
| 3 | +# Create your models here. |
+9-0backend/api/templates/api/serializers.html
| @@ -0,0 +1,9 @@ | ||
| 1 | +<pre>from rest_framework import serializers | |
| 2 | +from {{root_dir}}.models import {{model_name}} | |
| 3 | + | |
| 4 | + | |
| 5 | +class {{model_name}}Serializer(serializers.HyperlinkedModelSerializer): | |
| 6 | + class Meta: | |
| 7 | + model = {{model_name}} | |
| 8 | + fields = '__all__' | |
| 9 | +</pre> |
+11-0backend/api/templates/api/urls.html
| @@ -0,0 +1,11 @@ | ||
| 1 | +<pre>from django.urls import path, include | |
| 2 | +from api.{{api_name}}.views import {{model_name}}ViewSet | |
| 3 | +from rest_framework.routers import SimpleRouter | |
| 4 | + | |
| 5 | +router = SimpleRouter() | |
| 6 | +router.register(r'{{api_name}}', {{model_name}}ViewSet) | |
| 7 | + | |
| 8 | +urlpatterns = [ | |
| 9 | + path('', include(router.urls)), | |
| 10 | +] | |
| 11 | +</pre> |
+20-0backend/api/templates/api/viewset.html
| @@ -0,0 +1,20 @@ | ||
| 1 | +<pre>from rest_framework import viewsets | |
| 2 | +from rest_framework.schemas.openapi import AutoSchema | |
| 3 | +from rest_framework.permissions import AllowAny | |
| 4 | +from {{root_dir}}.models import {{model_name}} | |
| 5 | +from api.{{api_name}}.serializers import {{model_name}}Serializer | |
| 6 | + | |
| 7 | + | |
| 8 | +class {{model_name}}ViewSet(viewsets.ModelViewSet): | |
| 9 | + """ | |
| 10 | + API endpoint that allows {{model_name}} to be viewed or edited. | |
| 11 | + """ | |
| 12 | + schema = AutoSchema( | |
| 13 | + tags=['{{model_name}}'], | |
| 14 | + component_name='{{api_name}}', | |
| 15 | + operation_id_base='{{model_name}}' | |
| 16 | + ) | |
| 17 | + queryset = {{model_name}}.objects.all() | |
| 18 | + serializer_class = {{model_name}}Serializer | |
| 19 | + permission_classes = [AllowAny] | |
| 20 | +</pre> |
+31-0backend/api/templates/swagger-ui.html
| @@ -0,0 +1,31 @@ | ||
| 1 | +<!DOCTYPE html> | |
| 2 | +<html> | |
| 3 | + | |
| 4 | +<head> | |
| 5 | + <title>Swagger</title> | |
| 6 | + <meta charset="utf-8" /> | |
| 7 | + <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 8 | + <link rel="stylesheet" type="text/css" href="//unpkg.com/swagger-ui-dist@3/swagger-ui.css" /> | |
| 9 | +</head> | |
| 10 | + | |
| 11 | +<body> | |
| 12 | + <div id="swagger-ui"></div> | |
| 13 | + <script src="//unpkg.com/swagger-ui-dist@3/swagger-ui-bundle.js"></script> | |
| 14 | + <script> | |
| 15 | + const ui = SwaggerUIBundle({ | |
| 16 | + url: "{% url schema_url %}", | |
| 17 | + dom_id: '#swagger-ui', | |
| 18 | + presets: [ | |
| 19 | + SwaggerUIBundle.presets.apis, | |
| 20 | + SwaggerUIBundle.SwaggerUIStandalonePreset | |
| 21 | + ], | |
| 22 | + layout: "BaseLayout", | |
| 23 | + requestInterceptor: (request) => { | |
| 24 | + request.headers['X-CSRFToken'] = "{{ csrf_token }}" | |
| 25 | + return request; | |
| 26 | + } | |
| 27 | + }) | |
| 28 | + </script> | |
| 29 | +</body> | |
| 30 | + | |
| 31 | +</html> |
+3-0backend/api/tests.py
| @@ -0,0 +1,3 @@ | ||
| 1 | +from django.test import TestCase | |
| 2 | + | |
| 3 | +# Create your tests here. |
+14-0backend/api/urls.py
| @@ -0,0 +1,14 @@ | ||
| 1 | +from django.urls import include, re_path | |
| 2 | +from .views import ApiRoot, schema_view, swagger_view | |
| 3 | + | |
| 4 | + | |
| 5 | +urlpatterns = [ | |
| 6 | + # Auto-generated: a keyword used by the createapi command to know where to add new urls | |
| 7 | + re_path('$', ApiRoot.as_view(), name='api-root'), | |
| 8 | + re_path('', include('api.user.urls')), | |
| 9 | + re_path('openapi', schema_view, name='openapi-schema'), | |
| 10 | + re_path('swagger-ui', swagger_view, name='swagger-ui'), | |
| 11 | + re_path('openapi', schema_view, name='openapi-schema'), | |
| 12 | + re_path('swagger-ui', swagger_view, name='swagger-ui'), | |
| 13 | + re_path('auth/', include('api.auth.urls')), | |
| 14 | +] |
+9-0backend/api/user/serializers.py
| @@ -0,0 +1,9 @@ | ||
| 1 | +from rest_framework import serializers | |
| 2 | +from django.contrib.auth.models import User | |
| 3 | + | |
| 4 | + | |
| 5 | +class UserSerializer(serializers.HyperlinkedModelSerializer): | |
| 6 | + class Meta: | |
| 7 | + model = User | |
| 8 | + fields = ['url', 'id', 'username', 'first_name', 'last_name', 'email', 'password'] | |
| 9 | + extra_kwargs = {'password': {'write_only': True}} |
+10-0backend/api/user/urls.py
| @@ -0,0 +1,10 @@ | ||
| 1 | +from django.urls import path, include | |
| 2 | +from api.user.views import UserViewSet | |
| 3 | +from rest_framework.routers import SimpleRouter | |
| 4 | + | |
| 5 | +router = SimpleRouter() | |
| 6 | +router.register(r'user', UserViewSet) | |
| 7 | + | |
| 8 | +urlpatterns = [ | |
| 9 | + path('', include(router.urls)), | |
| 10 | +] |
+19-0backend/api/user/views.py
| @@ -0,0 +1,19 @@ | ||
| 1 | +from rest_framework import viewsets | |
| 2 | +from rest_framework.schemas.openapi import AutoSchema | |
| 3 | +from rest_framework.permissions import IsAuthenticated, AllowAny | |
| 4 | +from django.contrib.auth.models import User | |
| 5 | +from api.user.serializers import UserSerializer | |
| 6 | + | |
| 7 | + | |
| 8 | +class UserViewSet(viewsets.ModelViewSet): | |
| 9 | + """ | |
| 10 | + API endpoint that allows users to be viewed or edited. | |
| 11 | + """ | |
| 12 | + schema = AutoSchema( | |
| 13 | + tags=['User'], | |
| 14 | + component_name='user', | |
| 15 | + operation_id_base='User' | |
| 16 | + ) | |
| 17 | + queryset = User.objects.all().order_by('-date_joined') | |
| 18 | + serializer_class = UserSerializer | |
| 19 | + permission_classes = [AllowAny] |
+21-0backend/api/views.py
| @@ -0,0 +1,21 @@ | ||
| 1 | +from django.views.generic import TemplateView | |
| 2 | +from rest_framework.response import Response | |
| 3 | +from rest_framework.routers import APIRootView | |
| 4 | +from rest_framework.reverse import reverse | |
| 5 | +from rest_framework.schemas import get_schema_view | |
| 6 | + | |
| 7 | +schema_view = get_schema_view(title='Rango API') | |
| 8 | +swagger_view = TemplateView.as_view(template_name='swagger-ui.html', extra_context={'schema_url': 'openapi-schema'}) | |
| 9 | + | |
| 10 | + | |
| 11 | +# Create your views here. | |
| 12 | +class ApiRoot(APIRootView): | |
| 13 | + | |
| 14 | + def get(self, request, *args, **kwargs): | |
| 15 | + return Response({ | |
| 16 | + # Auto-generated: a keyword used by the createapi command to know where to add new urls | |
| 17 | + 'user': reverse('user-list', request=request), | |
| 18 | + 'openapi': reverse('openapi-schema', request=request), | |
| 19 | + 'swagger-ui': reverse('swagger-ui', request=request), | |
| 20 | + 'auth': reverse('api-auth-root', request=request), | |
| 21 | + }) |
+1-0backend/backend/settings.py
| @@ -41,6 +41,7 @@ INSTALLED_APPS = [ | ||
| 41 | 41 | 'django.contrib.messages', |
| 42 | 42 | 'django.contrib.staticfiles', |
| 43 | 43 | 'backend', |
| 44 | + 'api', | |
| 44 | 45 | ] |
| 45 | 46 | |
| 46 | 47 | MIDDLEWARE = [ |
+1-1backend/backend/urls.py
| @@ -14,7 +14,7 @@ Including another URLconf | ||
| 14 | 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) |
| 15 | 15 | """ |
| 16 | 16 | from django.contrib import admin |
| 17 | -from django.urls import path | |
| 17 | +from django.urls import path, include | |
| 18 | 18 | |
| 19 | 19 | urlpatterns = [ |
| 20 | 20 | path('admin/', admin.site.urls), |
+2-2docker-compose.yml
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | services: |
| 2 | - nppn: | |
| 2 | + djext: | |
| 3 | 3 | build: . |
| 4 | - container_name: nppn | |
| 4 | + container_name: djext | |
| 5 | 5 | volumes: |
| 6 | 6 | - ./postgres:/var/lib/postgresql/data |
| 7 | 7 | ports: |