Complete primitive backend
This commit is contained in:
256
django_backend/.gitignore
vendored
Normal file
256
django_backend/.gitignore
vendored
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/django,python
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=django,python
|
||||||
|
|
||||||
|
### Django ###
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
media
|
||||||
|
|
||||||
|
# If your build process includes running collectstatic, then you probably don't need or want to include staticfiles/
|
||||||
|
# in your Git repository. Update and uncomment the following line accordingly.
|
||||||
|
# <django-project-name>/staticfiles/
|
||||||
|
|
||||||
|
### Django.Python Stack ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
.pdm.toml
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
|
|
||||||
|
### Python ###
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/#use-with-ide
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/django,python
|
||||||
0
django_backend/api/__init__.py
Normal file
0
django_backend/api/__init__.py
Normal file
43
django_backend/api/admin.py
Normal file
43
django_backend/api/admin.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.contenttypes.admin import GenericTabularInline
|
||||||
|
from users.models import User
|
||||||
|
import nested_admin
|
||||||
|
|
||||||
|
from api.models import *
|
||||||
|
# Register your models here.
|
||||||
|
|
||||||
|
class IngredientInline(nested_admin.NestedGenericTabularInline):
|
||||||
|
model = Ingredient
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
class UserInline(nested_admin.NestedTabularInline):
|
||||||
|
model = User
|
||||||
|
fields = ("username",)
|
||||||
|
readonly_fields = ("username",)
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
def has_add_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def has_delete_permission(self, request, obj=None):
|
||||||
|
return False
|
||||||
|
|
||||||
|
class RecipeInline(nested_admin.NestedTabularInline):
|
||||||
|
model = Recipe
|
||||||
|
inlines = (IngredientInline,)
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
@admin.register(Recipe)
|
||||||
|
class RecipeAdmin(nested_admin.NestedModelAdmin):
|
||||||
|
list_display = ("name",)
|
||||||
|
inlines = (IngredientInline,)
|
||||||
|
|
||||||
|
@admin.register(Homegroup)
|
||||||
|
class HomegroupAdmin(nested_admin.NestedModelAdmin):
|
||||||
|
list_display = ("id", "name")
|
||||||
|
inlines = (UserInline, RecipeInline, IngredientInline)
|
||||||
|
|
||||||
|
@admin.register(List)
|
||||||
|
class ListAdmin(nested_admin.NestedModelAdmin):
|
||||||
|
list_display = ("id",)
|
||||||
|
inlines = (RecipeInline, IngredientInline)
|
||||||
6
django_backend/api/apps.py
Normal file
6
django_backend/api/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class ApiConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'api'
|
||||||
47
django_backend/api/migrations/0001_initial.py
Normal file
47
django_backend/api/migrations/0001_initial.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 4.1.3 on 2022-11-22 20:42
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('contenttypes', '0002_remove_content_type_name'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Homegroup',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='List',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='Recipe',
|
||||||
|
fields=[
|
||||||
|
('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(
|
||||||
|
name='Ingredient',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('object_id', models.PositiveBigIntegerField()),
|
||||||
|
('name', models.CharField(max_length=50)),
|
||||||
|
('in_stock', models.BooleanField(default=False)),
|
||||||
|
('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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
19
django_backend/api/migrations/0002_homegroup_name.py
Normal file
19
django_backend/api/migrations/0002_homegroup_name.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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,
|
||||||
|
),
|
||||||
|
]
|
||||||
0
django_backend/api/migrations/__init__.py
Normal file
0
django_backend/api/migrations/__init__.py
Normal file
33
django_backend/api/models.py
Normal file
33
django_backend/api/models.py
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
|
class Ingredient(models.Model):
|
||||||
|
limit = models.Q(app_label="api", model="recipe") | models.Q(app_label="api", model="list")
|
||||||
|
content_type = models.ForeignKey(ContentType, limit_choices_to=limit, on_delete=models.CASCADE)
|
||||||
|
object_id = models.PositiveBigIntegerField()
|
||||||
|
content_object = GenericForeignKey()
|
||||||
|
|
||||||
|
|
||||||
|
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 Homegroup(models.Model):
|
||||||
|
# Foreign Key Recipe -> Homegroup [as recipes]
|
||||||
|
# Foreign Key User -> Homegroup [as users]
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
|
||||||
|
|
||||||
|
class Recipe(models.Model):
|
||||||
|
name = models.CharField(max_length=50)
|
||||||
|
homegroup = models.ForeignKey(Homegroup, related_name="recipes", on_delete=models.CASCADE)
|
||||||
|
list = models.ForeignKey(List, related_name="recipes", on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
ingredients = GenericRelation(Ingredient, related_query_name="ingredients")
|
||||||
|
|
||||||
|
|
||||||
24
django_backend/api/serializers.py
Normal file
24
django_backend/api/serializers.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from api.models import *
|
||||||
|
|
||||||
|
class RecipeSerializer(serializers.ModelSerializer):
|
||||||
|
ingredients = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
|
||||||
|
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"]
|
||||||
|
|
||||||
|
class HomegroupSerializer(serializers.ModelSerializer):
|
||||||
|
users = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
recipes = serializers.PrimaryKeyRelatedField(many=True, read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Homegroup
|
||||||
|
fields = ["id", "recipes", "users"]
|
||||||
3
django_backend/api/tests.py
Normal file
3
django_backend/api/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
12
django_backend/api/urls.py
Normal file
12
django_backend/api/urls.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from django.urls import include, path
|
||||||
|
from rest_framework import routers
|
||||||
|
from api import views
|
||||||
|
|
||||||
|
router = routers.DefaultRouter()
|
||||||
|
router.register(r'recipes', views.RecipeView)
|
||||||
|
router.register(r'ingredients', views.IngredientView)
|
||||||
|
router.register(r'homegroups', views.HomegroupView)
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('', include(router.urls))
|
||||||
|
]
|
||||||
16
django_backend/api/views.py
Normal file
16
django_backend/api/views.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from rest_framework import viewsets
|
||||||
|
|
||||||
|
from api.serializers import *
|
||||||
|
from api.models import *
|
||||||
|
|
||||||
|
class RecipeView(viewsets.ModelViewSet):
|
||||||
|
serializer_class = RecipeSerializer
|
||||||
|
queryset = Recipe.objects.all()
|
||||||
|
|
||||||
|
class IngredientView(viewsets.ModelViewSet):
|
||||||
|
serializer_class = IngredientSerializer
|
||||||
|
queryset = Ingredient.objects.all()
|
||||||
|
|
||||||
|
class HomegroupView(viewsets.ModelViewSet):
|
||||||
|
serializer_class = HomegroupSerializer
|
||||||
|
queryset = Homegroup.objects.all()
|
||||||
0
django_backend/django_backend/__init__.py
Normal file
0
django_backend/django_backend/__init__.py
Normal file
16
django_backend/django_backend/asgi.py
Normal file
16
django_backend/django_backend/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
ASGI config for django_backend 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/4.1/howto/deployment/asgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_backend.settings')
|
||||||
|
|
||||||
|
application = get_asgi_application()
|
||||||
136
django_backend/django_backend/settings.py
Normal file
136
django_backend/django_backend/settings.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
Django settings for django_backend project.
|
||||||
|
|
||||||
|
Generated by 'django-admin startproject' using Django 4.1.3.
|
||||||
|
|
||||||
|
For more information on this file, see
|
||||||
|
https://docs.djangoproject.com/en/4.1/topics/settings/
|
||||||
|
|
||||||
|
For the full list of settings and their values, see
|
||||||
|
https://docs.djangoproject.com/en/4.1/ref/settings/
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# Quick-start development settings - unsuitable for production
|
||||||
|
# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/
|
||||||
|
|
||||||
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
|
SECRET_KEY = 'django-insecure-tz%&(g*jikac%ogq%vaf&%i!6m99q_lshu9g-&sz&bw8x!&zk3'
|
||||||
|
|
||||||
|
AUTH_USER_MODEL = 'users.User'
|
||||||
|
|
||||||
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
|
DEBUG = True
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = []
|
||||||
|
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
|
'rest_framework.authentication.TokenAuthentication',
|
||||||
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Application definition
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
'api',
|
||||||
|
'users',
|
||||||
|
'django.contrib.admin',
|
||||||
|
'django.contrib.auth',
|
||||||
|
'django.contrib.contenttypes',
|
||||||
|
'django.contrib.sessions',
|
||||||
|
'django.contrib.messages',
|
||||||
|
'django.contrib.staticfiles',
|
||||||
|
'rest_framework',
|
||||||
|
'rest_framework.authtoken',
|
||||||
|
'nested_admin'
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
'django.middleware.security.SecurityMiddleware',
|
||||||
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||||
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = 'django_backend.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 = 'django_backend.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#databases
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
'default': {
|
||||||
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Password validation
|
||||||
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/4.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
23
django_backend/django_backend/urls.py
Normal file
23
django_backend/django_backend/urls.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
"""django_backend URL Configuration
|
||||||
|
|
||||||
|
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||||
|
https://docs.djangoproject.com/en/4.1/topics/http/urls/
|
||||||
|
Examples:
|
||||||
|
Function views
|
||||||
|
1. Add an import: from my_app import views
|
||||||
|
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
||||||
|
Class-based views
|
||||||
|
1. Add an import: from other_app.views import Home
|
||||||
|
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
||||||
|
Including another URLconf
|
||||||
|
1. Import the include() function: from django.urls import include, path
|
||||||
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
|
"""
|
||||||
|
from django.contrib import admin
|
||||||
|
from django.urls import path, include
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/', include("api.urls")),
|
||||||
|
path('auth/', include("users.urls"))
|
||||||
|
]
|
||||||
16
django_backend/django_backend/wsgi.py
Normal file
16
django_backend/django_backend/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
WSGI config for django_backend 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/4.1/howto/deployment/wsgi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.core.wsgi import get_wsgi_application
|
||||||
|
|
||||||
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_backend.settings')
|
||||||
|
|
||||||
|
application = get_wsgi_application()
|
||||||
22
django_backend/manage.py
Executable file
22
django_backend/manage.py
Executable file
@@ -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', 'django_backend.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()
|
||||||
0
django_backend/users/__init__.py
Normal file
0
django_backend/users/__init__.py
Normal file
31
django_backend/users/admin.py
Normal file
31
django_backend/users/admin.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
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",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
("Important dates", {"fields": ("last_login", "date_joined")}),
|
||||||
|
)
|
||||||
|
add_fieldsets = (
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
{
|
||||||
|
"classes": ("wide",),
|
||||||
|
"fields": ("username", "password1", "password2", "homegroup"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
admin.site.register(User, CustomUserAdmin)
|
||||||
6
django_backend/users/apps.py
Normal file
6
django_backend/users/apps.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
|
class UsersConfig(AppConfig):
|
||||||
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
|
name = 'users'
|
||||||
47
django_backend/users/migrations/0001_initial.py
Normal file
47
django_backend/users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Generated by Django 4.1.3 on 2022-11-22 20:42
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0001_initial'),
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('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')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('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')),
|
||||||
|
('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')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
django_backend/users/migrations/__init__.py
Normal file
0
django_backend/users/migrations/__init__.py
Normal file
6
django_backend/users/models.py
Normal file
6
django_backend/users/models.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
from django.db import models
|
||||||
|
from api.models import Homegroup
|
||||||
|
|
||||||
|
class User(AbstractUser):
|
||||||
|
homegroup = models.ForeignKey(Homegroup, related_name="users", on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
28
django_backend/users/serializers.py
Normal file
28
django_backend/users/serializers.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
from rest_framework import serializers
|
||||||
|
from users.models import User
|
||||||
|
|
||||||
|
class UserSerializer(serializers.ModelSerializer): # https://stackoverflow.com/a/29867704/17834235
|
||||||
|
def create(self, validated_data):
|
||||||
|
user = User.objects.create()
|
||||||
|
|
||||||
|
user.set_password(validated_data["password"])
|
||||||
|
validated_data.pop("password")
|
||||||
|
|
||||||
|
for field in validated_data:
|
||||||
|
setattr(user, field, validated_data[field])
|
||||||
|
|
||||||
|
user.save()
|
||||||
|
return user
|
||||||
|
|
||||||
|
def update(self, instance, validated_data):
|
||||||
|
if "password" in validated_data:
|
||||||
|
password = validated_data.pop("password")
|
||||||
|
instance.set_password(password)
|
||||||
|
|
||||||
|
return super().update(instance, validated_data)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = ("id", "username", "email", "password")
|
||||||
|
write_only_fields = ("password",)
|
||||||
|
read_only_fields = ("id",)
|
||||||
3
django_backend/users/tests.py
Normal file
3
django_backend/users/tests.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
# Create your tests here.
|
||||||
11
django_backend/users/urls.py
Normal file
11
django_backend/users/urls.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from django.urls import path, include
|
||||||
|
from rest_framework.authtoken import views as authviews
|
||||||
|
from rest_framework import routers
|
||||||
|
|
||||||
|
from users import views
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
path('token', authviews.obtain_auth_token, name="api-token-auth"),
|
||||||
|
path('users', views.RegisterUserView.as_view()), # 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
|
||||||
|
]
|
||||||
36
django_backend/users/views.py
Normal file
36
django_backend/users/views.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from rest_framework import generics, permissions, views, status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from users import serializers, models
|
||||||
|
|
||||||
|
|
||||||
|
class IsOwner(permissions.BasePermission):
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
if obj == request.user:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# Anyone can register
|
||||||
|
class RegisterUserView(generics.CreateAPIView):
|
||||||
|
model = models.User
|
||||||
|
serializer_class = serializers.UserSerializer
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
|
||||||
|
# Allows user to modify their own data only
|
||||||
|
class ModifyUserView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
permission_classes = [
|
||||||
|
permissions.IsAuthenticated,
|
||||||
|
IsOwner
|
||||||
|
]
|
||||||
|
model = models.User
|
||||||
|
serializer_class = serializers.UserSerializer
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return models.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)
|
||||||
|
return Response(serializer.data)
|
||||||
BIN
planning/backend-structure.dia
Normal file
BIN
planning/backend-structure.dia
Normal file
Binary file not shown.
Reference in New Issue
Block a user