Account Management mostly implemented

This commit is contained in:
Alexander Laevens
2022-11-25 18:50:30 -07:00
parent 35fc396050
commit 839147e0b0
175 changed files with 7508 additions and 72 deletions

View File

@@ -22,6 +22,10 @@ class UserInline(nested_admin.NestedTabularInline):
def has_delete_permission(self, request, obj=None):
return False
class HomegroupInviteInline(nested_admin.NestedTabularInline):
model = HomegroupInvite
extra = 0
class RecipeInline(nested_admin.NestedTabularInline):
model = Recipe
inlines = (IngredientInline,)
@@ -35,9 +39,9 @@ class RecipeAdmin(nested_admin.NestedModelAdmin):
@admin.register(Homegroup)
class HomegroupAdmin(nested_admin.NestedModelAdmin):
list_display = ("id", "name")
inlines = (UserInline, RecipeInline, IngredientInline)
inlines = (UserInline, HomegroupInviteInline, RecipeInline, IngredientInline)
@admin.register(List)
class ListAdmin(nested_admin.NestedModelAdmin):
list_display = ("id",)
list_display = ("homegroup",)
inlines = (RecipeInline, IngredientInline)

View File

@@ -1,4 +1,4 @@
# Generated by Django 4.1.3 on 2022-11-22 20:42
# Generated by Django 4.1.3 on 2022-11-25 09:10
from django.db import migrations, models
import django.db.models.deletion
@@ -17,12 +17,13 @@ class Migration(migrations.Migration):
name='Homegroup',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
],
),
migrations.CreateModel(
name='List',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('homegroup', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='api.homegroup')),
],
),
migrations.CreateModel(
@@ -31,7 +32,6 @@ class Migration(migrations.Migration):
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('homegroup', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipes', to='api.homegroup')),
('list', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recipes', to='api.list')),
],
),
migrations.CreateModel(
@@ -44,4 +44,11 @@ class Migration(migrations.Migration):
('content_type', models.ForeignKey(limit_choices_to=models.Q(models.Q(('app_label', 'api'), ('model', 'recipe')), models.Q(('app_label', 'api'), ('model', 'list')), _connector='OR'), on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
],
),
migrations.CreateModel(
name='HomegroupInvite',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('homegroup', models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='api.homegroup')),
],
),
]

View File

@@ -1,19 +0,0 @@
# Generated by Django 4.1.3 on 2022-11-22 21:43
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='homegroup',
name='name',
field=models.CharField(default="Alex's Kitchen", max_length=50),
preserve_default=False,
),
]

View File

@@ -0,0 +1,37 @@
# Generated by Django 4.1.3 on 2022-11-25 09:10
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
('api', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='homegroupinvite',
name='user',
field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='invites', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='homegroup',
name='invited_users',
field=models.ManyToManyField(related_name='homegroup_invites', through='api.HomegroupInvite', to=settings.AUTH_USER_MODEL),
),
migrations.AddField(
model_name='recipe',
name='list',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='recipes', to='api.list'),
),
migrations.AlterUniqueTogether(
name='homegroupinvite',
unique_together={('homegroup', 'user')},
),
]

View File

@@ -13,15 +13,29 @@ class Ingredient(models.Model):
name = models.CharField(max_length=50)
in_stock = models.BooleanField(default=False)
class List(models.Model):
# Foreign Key Recipe -> List [as recipes]
extra_ingredients = GenericRelation(Ingredient, related_query_name="extra_ingredients")
class HomegroupInvite(models.Model):
homegroup = models.ForeignKey("api.Homegroup", on_delete=models.CASCADE, related_name="invites", blank=True)
user = models.ForeignKey("users.User", on_delete=models.CASCADE, related_name="invites", blank=True)
class Meta:
unique_together = ("homegroup", "user")
class Homegroup(models.Model):
# Foreign Key Recipe -> Homegroup [as recipes]
# Foreign Key User -> Homegroup [as users]
name = models.CharField(max_length=50)
invited_users = models.ManyToManyField("users.User", related_name="homegroup_invites", through=HomegroupInvite)
def __repr__(self):
return f"{self.id}: {self.name}"
def __str__(self):
return f"{self.id}: {self.name}"
class List(models.Model):
# Foreign Key Recipe -> List [as recipes]
extra_ingredients = GenericRelation(Ingredient, related_query_name="extra_ingredients")
homegroup = models.OneToOneField(Homegroup, on_delete=models.CASCADE, primary_key=True)
class Recipe(models.Model):

View File

@@ -1,24 +1,42 @@
from rest_framework import serializers
from api.models import *
from users.serializers import UserSerializer
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = ["id", "name", "in_stock", "content_type", "object_id"]
read_only_fields = ["id"]
class RecipeSerializer(serializers.ModelSerializer):
ingredients = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
# ingredients = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
# ingredients = IngredientSerializer(many=True, read_only=True)
ingredients = serializers.SerializerMethodField()
class Meta:
model = Recipe
fields = ["id", "name", "ingredients"]
read_only_fields = ["id"]
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = ["id", "name", "in_stock"]
read_only_fields = ["id"]
def get_ingredients(self, instance):
ingredients = instance.ingredients.all().order_by("name")
return IngredientSerializer(ingredients, many=True).data
class HomegroupSerializer(serializers.ModelSerializer):
users = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
recipes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
def create(self, validated_data):
homegroup = super().create(validated_data)
List.objects.get_or_create(homegroup=homegroup)
return homegroup
class Meta:
model = Homegroup
fields = ["id", "recipes", "users"]
fields = ["id", "name", "recipes", "users", "invites"]
read_only_fields = ["recipes", "users", "invites"]
class InviteSerializer(serializers.ModelSerializer):
class Meta:
model = HomegroupInvite
fields = ["id", "homegroup", "user"]

View File

@@ -6,6 +6,7 @@ router = routers.DefaultRouter()
router.register(r'recipes', views.RecipeView)
router.register(r'ingredients', views.IngredientView)
router.register(r'homegroups', views.HomegroupView)
router.register(r'groupinvites', views.HomegroupInviteView)
urlpatterns = [
path('', include(router.urls))

View File

@@ -14,3 +14,7 @@ class IngredientView(viewsets.ModelViewSet):
class HomegroupView(viewsets.ModelViewSet):
serializer_class = HomegroupSerializer
queryset = Homegroup.objects.all()
class HomegroupInviteView(viewsets.ModelViewSet):
serializer_class = InviteSerializer
queryset = HomegroupInvite.objects.all()

View File

@@ -14,6 +14,9 @@ from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
MEDIA_ROOT = BASE_DIR.joinpath("media")
STATIC_URL = "static/"
MEDIA_URL = "/media/"
# Quick-start development settings - unsuitable for production
@@ -27,7 +30,9 @@ AUTH_USER_MODEL = 'users.User'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
ALLOWED_HOSTS = ["192.168.0.16", "127.0.0.1", "localhost"]
# CORS_ALLOWED_ORIGINS = ["http://192.168.0.16:8000"]
CORS_ALLOW_ALL_ORIGINS = True
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
@@ -47,6 +52,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'corsheaders',
'rest_framework',
'rest_framework.authtoken',
'nested_admin'
@@ -55,6 +61,7 @@ INSTALLED_APPS = [
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
@@ -124,12 +131,6 @@ USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/4.1/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field

View File

@@ -15,9 +15,12 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include("api.urls")),
path('auth/', include("users.urls"))
]
path('auth/', include("users.urls")),
path('api-auth/', include('rest_framework.urls')),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -4,18 +4,18 @@ from .models import User
class CustomUserAdmin(UserAdmin):
fieldsets = (
(None, {"fields": ("username", "password", "homegroup")}),
("Personal info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
),
},
),
(None, {"fields": ("username", "password", "image", "homegroup")}),
("Personal info", {"fields": ("first_name", "last_name")}),
# (
# "Permissions",
# {
# "fields": (
# "is_active",
# "is_staff",
# "is_superuser",
# ),
# },
# ),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
add_fieldsets = (

View File

@@ -1,10 +1,11 @@
# Generated by Django 4.1.3 on 2022-11-22 20:42
# Generated by Django 4.1.3 on 2022-11-25 09:10
import django.contrib.auth.models
import django.contrib.auth.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import users.models
class Migration(migrations.Migration):
@@ -12,8 +13,8 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
('api', '0001_initial'),
('auth', '0012_alter_user_first_name_max_length'),
('api', '0001_initial'),
]
operations = [
@@ -31,6 +32,7 @@ class Migration(migrations.Migration):
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('image', models.ImageField(blank=True, null=True, upload_to=users.models.image_path)),
('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')),
('homegroup', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='users', to='api.homegroup')),
('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')),

View File

@@ -1,6 +1,13 @@
from django.contrib.auth.models import AbstractUser
from django.db import models
from api.models import Homegroup
from pathlib import Path
def image_path(instance, fname):
extension = Path(fname).suffix
return f"profile-images/{instance.id}{extension}"
class User(AbstractUser):
homegroup = models.ForeignKey(Homegroup, related_name="users", on_delete=models.SET_NULL, blank=True, null=True)
homegroup = models.ForeignKey(Homegroup, related_name="users", on_delete=models.SET_NULL, blank=True, null=True)
image = models.ImageField(upload_to=image_path, null=True, blank=True)

View File

@@ -23,6 +23,6 @@ class UserSerializer(serializers.ModelSerializer): # https://stackoverflow.com/
class Meta:
model = User
fields = ("id", "username", "email", "password")
fields = ("id", "username", "first_name", "last_name", "password", "image", "homegroup", "homegroup_invites")
write_only_fields = ("password",)
read_only_fields = ("id",)
read_only_fields = ("id", "homegroup_invites")

View File

@@ -1,11 +1,14 @@
from django.urls import path, include
from rest_framework.authtoken import views as authviews
from rest_framework import routers
from users import views
router = routers.DefaultRouter()
router.register(r'users', views.UsersListView)
urlpatterns = [
path('', include(router.urls)),
path('token', authviews.obtain_auth_token, name="api-token-auth"),
path('users', views.RegisterUserView.as_view()), # Exposes POST to everyone for registering
# path('users', views.RegisterUserView), # Exposes POST to everyone for registering
path('users/me', views.ModifyUserView.as_view()) # exposes GET / PUT / PATCH / DELETE for registered users for their user object
]

View File

@@ -1,7 +1,10 @@
from rest_framework import generics, permissions, views, status
from rest_framework import generics, permissions, viewsets, mixins, filters, pagination
from rest_framework.response import Response
from users import serializers, models
from users.models import *
from users.serializers import *
class Pagination(pagination.PageNumberPagination):
page_size = 4
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
@@ -12,10 +15,17 @@ class IsOwner(permissions.BasePermission):
# Anyone can register
class RegisterUserView(generics.CreateAPIView):
model = models.User
serializer_class = serializers.UserSerializer
class UsersListView(mixins.CreateModelMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
model = User
serializer_class = UserSerializer
permission_classes = [permissions.AllowAny]
queryset = User.objects.all().order_by("first_name", "last_name", "username")
filter_backends = [filters.SearchFilter]
search_fields = ["username", "first_name", "last_name"]
pagination_class = Pagination
def get_serializer(self, *args, **kwargs):
return super().get_serializer(*args, **kwargs)
# Allows user to modify their own data only
@@ -24,13 +34,13 @@ class ModifyUserView(generics.RetrieveUpdateDestroyAPIView):
permissions.IsAuthenticated,
IsOwner
]
model = models.User
serializer_class = serializers.UserSerializer
model = User
serializer_class = UserSerializer
def get_object(self):
return models.User.objects.get(pk=self.request.user.id)
return User.objects.get(pk=self.request.user.id)
def retrieve(self, request, *args, **kwargs):
user = models.User.objects.get(pk=request.user.id)
serializer = serializers.UserSerializer(user)
user = User.objects.get(pk=request.user.id)
serializer = UserSerializer(user, context={"request":request})
return Response(serializer.data)