Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion answerking/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Django base settings for answerking project..
"""
import os
from datetime import timedelta
from os.path import join
from pathlib import Path

Expand Down Expand Up @@ -39,6 +40,7 @@
"rest_framework",
"corsheaders",
"drf_problems",
"rest_framework_simplejwt.token_blacklist",
]

MIDDLEWARE = [
Expand Down Expand Up @@ -81,6 +83,9 @@
"EXCEPTION_HANDLER": "answerking_app.utils.exceptions_handler.wrapper",
"COERCE_DECIMAL_TO_STRING": False,
"DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%S.%fZ",
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
}

# Database
Expand All @@ -97,7 +102,7 @@
}
}

# Password validation
# Password permissions
# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
Expand All @@ -120,6 +125,13 @@
# Internationalization
# https://docs.djangoproject.com/en/4.0/topics/i18n/

SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=30),
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"UPDATE_LAST_LOGIN": True,
}
LANGUAGE_CODE = "en-us"

TIME_ZONE = "UTC"
Expand Down
5 changes: 3 additions & 2 deletions answerking/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
path("api/", include("answerking_app.urls.category_urls")),
path("api/", include("answerking_app.urls.order_urls")),
path("api/", include("answerking_app.urls.tag_urls")),
path("api/", include("answerking_app.urls.auth_urls")),
path("admin/", admin.site.urls),
path("", include("drf_problems.urls")),
]
Expand All @@ -19,9 +20,9 @@
)

urlpatterns += [
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path("api/schema", SpectacularAPIView.as_view(), name="schema"),
path(
"api/schema/swagger-ui/",
"api/schema/swagger-ui",
SpectacularSwaggerView.as_view(url_name="schema"),
name="swagger-ui",
),
Expand Down
4 changes: 4 additions & 0 deletions answerking_app/models/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from decimal import Decimal

from django.contrib.auth.models import User
from django.db import models


Expand Down Expand Up @@ -45,6 +46,9 @@ class Status(models.TextChoices):
)
created_on = models.DateTimeField(auto_now_add=True)
last_updated = models.DateTimeField(auto_now=True)
owner = models.ForeignKey(
"auth.User", related_name="orders", on_delete=models.CASCADE
)

def calculate_total(self):
total = Decimal(0.00)
Expand Down
30 changes: 30 additions & 0 deletions answerking_app/models/permissions/auth_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from rest_framework.permissions import IsAdminUser, BasePermission


class IsStaffUser(IsAdminUser):
"""
Allows access to Staff and Managers only.
"""

pass


class IsManagerUser(BasePermission):
Copy link
Contributor

@JoeCSykes JoeCSykes Feb 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the same as IsStaffUser so can get rid of this

"""
Allows access to Manager's only.
"""

def has_permission(self, request, view):
return bool(request.user and request.user.is_staff)


class IsOwner(BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""

def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)

def has_object_permission(self, request, view, obj):
return bool(obj.owner == request.user)
91 changes: 88 additions & 3 deletions answerking_app/models/serializers.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import re
from abc import ABC
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this?

from typing import OrderedDict

from django.contrib.auth.models import User
from django.contrib.auth.password_validation import validate_password
from django.core.validators import (
MaxValueValidator,
MinValueValidator,
RegexValidator,
)
from rest_framework import serializers, status
from rest_framework.validators import UniqueValidator
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer

from answerking_app.models.models import (
Category,
Expand Down Expand Up @@ -246,9 +252,10 @@ class OrderSerializer(serializers.ModelSerializer):
lineItems = LineItemSerializer(
source="lineitem_set", many=True, required=False
)
owner = serializers.ReadOnlyField(source="owner.username")

def create(self, validated_data: dict) -> Order:
order: Order = Order.objects.create()
order: Order = Order.objects.create(owner=validated_data["owner"])
if "lineitem_set" in validated_data:
line_items_data = validated_data["lineitem_set"]
self.create_order_line_items(
Expand Down Expand Up @@ -298,17 +305,95 @@ def validate_lineItems(self, line_items_data):

class Meta:
model = Order
fields = (
fields = [
"id",
"createdOn",
"lastUpdated",
"orderStatus",
"orderTotal",
"lineItems",
)
"owner",
]
depth = 3


class ManagerAuthSerializer(serializers.ModelSerializer):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going with the permission as "IsStaffUser" we may want to change this to StaffAuthSerializer. Similar with a few other naming conventions further down also.

username = serializers.CharField(
required=True,
validators=[UniqueValidator(queryset=User.objects.all())],
max_length=50,
min_length=4,
)
email = serializers.EmailField(
required=True,
validators=[UniqueValidator(queryset=User.objects.all())],
)
password = serializers.CharField(
write_only=True, required=True, validators=[validate_password]
)
password2 = serializers.CharField(
write_only=True,
required=True,
)

class Meta:
model = User
fields = (
"username",
"password",
"password2",
Comment on lines +343 to +344
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 single password should be sent, password confirmation should be done by the front end

"email",
"first_name",
"last_name",
)

def validate(self, attrs):
if attrs["password"] != attrs["password2"]:
raise ProblemDetails(
status=status.HTTP_400_BAD_REQUEST,
detail="The passwords supplied do not match",
)
return attrs

def validate_first_name(self, value: str) -> str:
return compress_white_spaces(value)

def validate_last_name(self, value: str) -> str:
return compress_white_spaces(value)

def create(self, validated_data):
user = User.objects.create(
username=validated_data["username"],
email=validated_data["email"],
first_name=validated_data["first_name"],
last_name=validated_data["last_name"],
is_staff=True,
is_superuser=False,
)

user.set_password(validated_data["password"])
user.save()
return user


class LoginSerializer(TokenObtainPairSerializer):
@classmethod
def get_token(cls, user):
token = super().get_token(user)
token["username"] = user.username
token["email"] = user.email
return token

def validate(self, attrs):
attrs = super().validate(attrs)
return {
"username": self.user.username,
"email": self.user.email,
"first_name": self.user.first_name,
**attrs,
}


class ErrorDetailSerializer(serializers.Serializer):
name = serializers.CharField()

Expand Down
17 changes: 17 additions & 0 deletions answerking_app/urls/auth_urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from functools import partial

from django.urls import path
from rest_framework_simplejwt.views import TokenRefreshView, TokenBlacklistView

from answerking_app.views import auth_views

urlpatterns: list[partial] = [
path(
"register/manager",
auth_views.RegisterManagerView.as_view(),
name="register_manager",
),
path("login", auth_views.LoginView.as_view(), name="token_obtain_pair"),
path("login/refresh", TokenRefreshView.as_view(), name="token_refresh"),
path("logout", TokenBlacklistView.as_view(), name="token_blacklist"),
]
12 changes: 6 additions & 6 deletions answerking_app/urls/category_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@
urlpatterns: list[partial] = [
path(
"categories",
category_views.CategoryListView.as_view(),
name="category_list",
category_views.CategoryView.as_view(),
name="category",
),
path(
"categories/<pk>",
category_views.CategoryDetailView.as_view(),
name="category_detail",
category_views.CategoryIdView.as_view(),
name="category_id",
),
path(
"categories/<pk>/products",
category_views.CategoryProductListView.as_view(),
name="category_product_list",
category_views.CategoryProductView.as_view(),
name="category_product",
),
]
2 changes: 1 addition & 1 deletion answerking_app/urls/order_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from answerking_app.views import order_views

urlpatterns: list[partial] = [
path("orders", order_views.OrderListView.as_view(), name="order_list"),
path("orders", order_views.OrderView.as_view(), name="order_list"),
path(
"orders/<pk>",
order_views.OrderDetailView.as_view(),
Expand Down
2 changes: 1 addition & 1 deletion answerking_app/urls/product_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
urlpatterns: list[partial] = [
path(
"products",
product_views.ProductListView.as_view(),
product_views.ProductView.as_view(),
name="product_list",
),
path(
Expand Down
2 changes: 1 addition & 1 deletion answerking_app/utils/schema/schema_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@
problem_detail_example: ProblemDetails = {
"errors": {"name": "The name field is required."},
"type": "https://testserver/problems/error/",
"title": "One or more validation errors occurred.",
"title": "One or more permissions errors occurred.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably would change this to "One or more validation or permissions errors occurred."

"status": 400,
"traceID": "00-f40e09a437a87f4ebcd2f39b128bb8f3-4b2ad798ac046140-00",
"detail": "string",
Expand Down
21 changes: 21 additions & 0 deletions answerking_app/views/auth_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from rest_framework import generics
from rest_framework.permissions import AllowAny
from rest_framework_simplejwt.views import TokenObtainPairView

from answerking_app.models.serializers import (
ManagerAuthSerializer,
LoginSerializer,
)


class RegisterManagerView(
generics.CreateAPIView,
):
permission_classes = (AllowAny,)
Copy link
Contributor

@JoeCSykes JoeCSykes Feb 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be "IsStaffUser" rather than AllowAny?

serializer_class = ManagerAuthSerializer


class LoginView(
TokenObtainPairView,
):
serializer_class = LoginSerializer
Loading