Skip to content
Snippets Groups Projects
Commit a2684067 authored by Sigurd Røstad Augdal's avatar Sigurd Røstad Augdal Committed by Rolf Erik Sesseng Aas
Browse files

First real commit. Ref #1 #2 #4 #5 #7 #8

Long list of changes:
Add Mealtype and Diet to navbar
Add Mealtype and Diet pages
Add Mealtype and Diet in backend
Add Mealtype and Diet in db
parent fabaeb76
No related branches found
No related tags found
2 merge requests!4Develop,!2Diet (frontend and backend)
Showing
with 1366 additions and 0 deletions
"""Module for registering models from workouts app to admin page so that they appear
"""
from django.contrib import admin
# Register your models here.
from .models import Mealtype, MealtypeInstance, Diet, DietFile
admin.site.register(Mealtype)
admin.site.register(MealtypeInstance)
admin.site.register(Diet)
admin.site.register(DietFile)
"""AppConfig for diets app
"""
from django.apps import AppConfig
class DietsConfig(AppConfig):
"""AppConfig for diets app
Attributes:
name (str): The name of the application
"""
name = "diets"
# Generated by Django 3.1 on 2020-08-21 03:42
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import diets.models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="Mealtype",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("description", models.TextField()),
],
),
migrations.CreateModel(
name="Diet",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("name", models.CharField(max_length=100)),
("date", models.DateTimeField()),
("notes", models.TextField()),
(
"visibility",
models.CharField(
choices=[("PU", "Public"), ("CO", "Coach"), ("PR", "Private")],
default="CO",
max_length=2,
),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="diets",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ["-date"],
},
),
migrations.CreateModel(
name="DietFile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"file",
models.FileField(upload_to=diets.models.diet_directory_path),
),
(
"owner",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="files",
to=settings.AUTH_USER_MODEL,
),
),
(
"diet",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="files",
to="diets.diet",
),
),
],
),
migrations.CreateModel(
name="MealtypeInstance",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("sets", models.IntegerField()),
("number", models.IntegerField()),
(
"mealtype",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="instances",
to="diets.mealtype",
),
),
(
"diet",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="mealtype_instances",
to="diets.diet",
),
),
],
),
]
# Generated by Django 3.1 on 2020-09-10 00:22
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("diets", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="dietfile",
name="owner",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="diet_files",
to=settings.AUTH_USER_MODEL,
),
),
]
# Generated by Django 3.1 on 2021-02-04 10:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('diets', '0002_auto_20200910_0222'),
]
operations = [
migrations.CreateModel(
name='RememberMe',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('remember_me', models.CharField(max_length=500)),
],
),
]
"""
Mixins for the diets application
"""
class CreateListModelMixin(object):
"""Mixin that allows to create multiple objects from lists.
Taken from https://stackoverflow.com/a/48885641
"""
def get_serializer(self, *args, **kwargs):
"""If an array is passed, set serializer to many.
kwargs["many"] will be set to true if an array is passed. This argument
is passed when retrieving the serializer.
Args:
*args: Variable length argument list passed to the serializer.
**kwargs: Arbitrary keyword arguments passed to the serializer, including "many".
Returns:
[type]: [description]
"""
if isinstance(kwargs.get("data", {}), list):
kwargs["many"] = True
return super(CreateListModelMixin, self).get_serializer(*args, **kwargs)
"""Contains the models for the diets Django application. Users
log diets (Diet), which contain instances (MealtypeInstance) of various
type of mealtypes (Mealtype). The user can also upload files (DietFile) .
"""
import os
from django.db import models
from django.core.files.storage import FileSystemStorage
from django.conf import settings
from django.contrib.auth import get_user_model
class OverwriteStorage(FileSystemStorage):
"""Filesystem storage for overwriting files. Currently unused."""
def get_available_name(self, name, max_length=None):
"""https://djangosnippets.org/snippets/976/
Returns a filename that's free on the target storage system, and
available for new content to be written to.
Args:
name (str): Name of the file
max_length (int, optional): Maximum length of a file name. Defaults to None.
"""
if self.exists(name):
os.remove(os.path.join(settings.MEDIA_ROOT, name))
# Create your models here.
class Diet(models.Model):
"""Django model for a diet that users can log.
A diet has several attributes, and is associated with one or more mealtypes
(instances) and, optionally, files uploaded by the user.
Attributes:
name: Name of the diet
date: Date the diet was performed or is planned
notes: Notes about the diet
owner: User that logged the diet
visibility: The visibility level of the diet: Public, Coach, or Private
"""
name = models.CharField(max_length=100)
date = models.DateTimeField()
notes = models.TextField()
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="diets"
)
# Visibility levels
PUBLIC = "PU" # Visible to all authenticated users
COACH = "CO" # Visible only to owner and their coach
PRIVATE = "PR" # Visible only to owner
VISIBILITY_CHOICES = [
(PUBLIC, "Public"),
(COACH, "Coach"),
(PRIVATE, "Private"),
] # Choices for visibility level
visibility = models.CharField(
max_length=2, choices=VISIBILITY_CHOICES, default=COACH
)
class Meta:
ordering = ["-date"]
def __str__(self):
return self.name
class Mealtype(models.Model):
"""Django model for an mealtype type that users can create.
Each mealtype instance must have an mealtype type, e.g., Pushups, Crunches, or Lunges.
Attributes:
name: Name of the mealtype type
description: Description of the mealtype type
"""
name = models.CharField(max_length=100)
description = models.TextField()
def __str__(self):
return self.name
class MealtypeInstance(models.Model):
"""Django model for an instance of an mealtype.
Each diet has one or more mealtype instances, each of a given type. For example,
Kyle's diet on 15.06.2029 had one mealtype instance: 3 (sets) reps (unit) of
10 (number) pushups (mealtype type)
Attributes:
diet: The diet associated with this mealtype instance
mealtype: The mealtype type of this instance
sets: The number of sets the owner will perform/performed
number: The number of repetitions in each set the owner will perform/performed
"""
diet = models.ForeignKey(
Diet, on_delete=models.CASCADE, related_name="mealtype_instances"
)
mealtype = models.ForeignKey(
Mealtype, on_delete=models.CASCADE, related_name="instances"
)
sets = models.IntegerField()
number = models.IntegerField()
def diet_directory_path(instance, filename):
"""Return path for which diet files should be uploaded on the web server
Args:
instance (DietFile): DietFile instance
filename (str): Name of the file
Returns:
str: Path where diet file is stored
"""
return f"diets/{instance.diet.id}/{filename}"
class DietFile(models.Model):
"""Django model for file associated with a diet. Basically a wrapper.
Attributes:
diet: The diet for which this file has been uploaded
owner: The user who uploaded the file
file: The actual file that's being uploaded
"""
diet = models.ForeignKey(Diet, on_delete=models.CASCADE, related_name="files")
owner = models.ForeignKey(
get_user_model(), on_delete=models.CASCADE, related_name="diet_files"
)
file = models.FileField(upload_to=diet_directory_path)
class RememberMe(models.Model):
"""Django model for an remember_me cookie used for remember me functionality.
Attributes:
remember_me: Value of cookie used for remember me
"""
remember_me = models.CharField(max_length=500)
def __str__(self):
return self.remember_me
"""Contains custom parsers for serializers from the diets Django app
"""
import json
from rest_framework import parsers
# Thanks to https://stackoverflow.com/a/50514630
class MultipartJsonParser(parsers.MultiPartParser):
"""Parser for serializing multipart data containing both files and JSON.
This is currently unused.
"""
def parse(self, stream, media_type=None, parser_context=None):
result = super().parse(
stream, media_type=media_type, parser_context=parser_context
)
data = {}
new_files = {"files": []}
# for case1 with nested serializers
# parse each field with json
for key, value in result.data.items():
if not isinstance(value, str):
data[key] = value
continue
if "{" in value or "[" in value:
try:
data[key] = json.loads(value)
except ValueError:
data[key] = value
else:
data[key] = value
files = result.files.getlist("files")
for file in files:
new_files["files"].append({"file": file})
return parsers.DataAndFiles(data, new_files)
"""Contains custom DRF permissions classes for the diets app
"""
from rest_framework import permissions
from diets.models import Diet
class IsOwner(permissions.BasePermission):
"""Checks whether the requesting user is also the owner of the existing object"""
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
class IsOwnerOfDiet(permissions.BasePermission):
"""Checks whether the requesting user is also the owner of the new or existing object"""
def has_permission(self, request, view):
if request.method == "POST":
if request.data.get("diet"):
diet_id = request.data["diet"].split("/")[-2]
diet = Diet.objects.get(pk=diet_id)
if diet:
return diet.owner == request.user
return False
return True
def has_object_permission(self, request, view, obj):
return obj.diet.owner == request.user
class IsCoachAndVisibleToCoach(permissions.BasePermission):
"""Checks whether the requesting user is the existing object's owner's coach
and whether the object (diet) has a visibility of Public or Coach.
"""
def has_object_permission(self, request, view, obj):
return obj.owner.coach == request.user
class IsCoachOfDietAndVisibleToCoach(permissions.BasePermission):
"""Checks whether the requesting user is the existing diet's owner's coach
and whether the object has a visibility of Public or Coach.
"""
def has_object_permission(self, request, view, obj):
return obj.diet.owner.coach == request.user
class IsPublic(permissions.BasePermission):
"""Checks whether the object (diet) has visibility of Public."""
def has_object_permission(self, request, view, obj):
return obj.visibility == "PU"
class IsDietPublic(permissions.BasePermission):
"""Checks whether the object's diet has visibility of Public."""
def has_object_permission(self, request, view, obj):
return obj.diet.visibility == "PU"
class IsReadOnly(permissions.BasePermission):
"""Checks whether the HTTP request verb is only for retrieving data (GET, HEAD, OPTIONS)"""
def has_object_permission(self, request, view, obj):
return request.method in permissions.SAFE_METHODS
"""Serializers for the diets application
"""
from rest_framework import serializers
from rest_framework.serializers import HyperlinkedRelatedField
from diets.models import Diet, Mealtype, MealtypeInstance, DietFile, RememberMe
class MealtypeInstanceSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for an MealtypeInstance. Hyperlinks are used for relationships by default.
Serialized fields: url, id, mealtype, sets, number, diet
Attributes:
diet: The associated diet for this instance, represented by a hyperlink
"""
diet = HyperlinkedRelatedField(
queryset=Diet.objects.all(), view_name="diet-detail", required=False
)
class Meta:
model = MealtypeInstance
fields = ["url", "id", "mealtype", "sets", "number", "diet"]
class DietFileSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for a DietFile. Hyperlinks are used for relationships by default.
Serialized fields: url, id, owner, file, diet
Attributes:
owner: The owner (User) of the DietFile, represented by a username. ReadOnly
diet: The associate diet for this DietFile, represented by a hyperlink
"""
owner = serializers.ReadOnlyField(source="owner.username")
diet = HyperlinkedRelatedField(
queryset=Diet.objects.all(), view_name="diet-detail", required=False
)
class Meta:
model = DietFile
fields = ["url", "id", "owner", "file", "diet"]
def create(self, validated_data):
return DietFile.objects.create(**validated_data)
class DietSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for a Diet. Hyperlinks are used for relationships by default.
This serializer specifies nested serialization since a diet consists of DietFiles
and MealtypeInstances.
Serialized fields: url, id, name, date, notes, owner, owner_username, visiblity,
mealtype_instances, files
Attributes:
owner_username: Username of the owning User
mealtype_instance: Serializer for ExericseInstances
files: Serializer for DietFiles
"""
owner_username = serializers.SerializerMethodField()
mealtype_instances = MealtypeInstanceSerializer(many=True, required=True)
files = DietFileSerializer(many=True, required=False)
class Meta:
model = Diet
fields = [
"url",
"id",
"name",
"date",
"notes",
"owner",
"owner_username",
"visibility",
"mealtype_instances",
"files",
]
extra_kwargs = {"owner": {"read_only": True}}
def create(self, validated_data):
"""Custom logic for creating MealtypeInstances, DietFiles, and a Diet.
This is needed to iterate over the files and mealtype instances, since this serializer is
nested.
Args:
validated_data: Validated files and mealtype_instances
Returns:
Diet: A newly created Diet
"""
mealtype_instances_data = validated_data.pop("mealtype_instances")
files_data = []
if "files" in validated_data:
files_data = validated_data.pop("files")
diet = Diet.objects.create(**validated_data)
for mealtype_instance_data in mealtype_instances_data:
MealtypeInstance.objects.create(diet=diet, **mealtype_instance_data)
for file_data in files_data:
DietFile.objects.create(
diet=diet, owner=diet.owner, file=file_data.get("file")
)
return diet
def update(self, instance, validated_data):
"""Custom logic for updating a Diet with its MealtypeInstances and Diets.
This is needed because each object in both mealtype_instances and files must be iterated
over and handled individually.
Args:
instance (Diet): Current Diet object
validated_data: Contains data for validated fields
Returns:
Diet: Updated Diet instance
"""
mealtype_instances_data = validated_data.pop("mealtype_instances")
mealtype_instances = instance.mealtype_instances
instance.name = validated_data.get("name", instance.name)
instance.notes = validated_data.get("notes", instance.notes)
instance.visibility = validated_data.get("visibility", instance.visibility)
instance.date = validated_data.get("date", instance.date)
instance.save()
# Handle MealtypeInstances
# This updates existing mealtype instances without adding or deleting object.
# zip() will yield n 2-tuples, where n is
# min(len(mealtype_instance), len(mealtype_instance_data))
for mealtype_instance, mealtype_instance_data in zip(
mealtype_instances.all(), mealtype_instances_data
):
mealtype_instance.mealtype = mealtype_instance_data.get(
"mealtype", mealtype_instance.mealtype
)
mealtype_instance.number = mealtype_instance_data.get(
"number", mealtype_instance.number
)
mealtype_instance.sets = mealtype_instance_data.get(
"sets", mealtype_instance.sets
)
mealtype_instance.save()
# If new mealtype instances have been added to the diet, then create them
if len(mealtype_instances_data) > len(mealtype_instances.all()):
for i in range(len(mealtype_instances.all()), len(mealtype_instances_data)):
mealtype_instance_data = mealtype_instances_data[i]
MealtypeInstance.objects.create(
diet=instance, **mealtype_instance_data
)
# Else if mealtype instances have been removed from the diet, then delete them
elif len(mealtype_instances_data) < len(mealtype_instances.all()):
for i in range(len(mealtype_instances_data), len(mealtype_instances.all())):
mealtype_instances.all()[i].delete()
# Handle DietFiles
if "files" in validated_data:
files_data = validated_data.pop("files")
files = instance.files
for file, file_data in zip(files.all(), files_data):
file.file = file_data.get("file", file.file)
# If new files have been added, creating new DietFiles
if len(files_data) > len(files.all()):
for i in range(len(files.all()), len(files_data)):
DietFile.objects.create(
diet=instance,
owner=instance.owner,
file=files_data[i].get("file"),
)
# Else if files have been removed, delete DietFiles
elif len(files_data) < len(files.all()):
for i in range(len(files_data), len(files.all())):
files.all()[i].delete()
return instance
def get_owner_username(self, obj):
"""Returns the owning user's username
Args:
obj (Diet): Current Diet
Returns:
str: Username of owner
"""
return obj.owner.username
class MealtypeSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for an Mealtype. Hyperlinks are used for relationships by default.
Serialized fields: url, id, name, description, instances
Attributes:
instances: Associated mealtype instances with this Mealtype type. Hyperlinks.
"""
instances = serializers.HyperlinkedRelatedField(
many=True, view_name="mealtypeinstance-detail", read_only=True
)
class Meta:
model = Mealtype
fields = ["url", "id", "name", "description", "instances"]
class RememberMeSerializer(serializers.HyperlinkedModelSerializer):
"""Serializer for an RememberMe. Hyperlinks are used for relationships by default.
Serialized fields: remember_me
Attributes:
remember_me: Value of cookie used for remember me functionality
"""
class Meta:
model = RememberMe
fields = ["remember_me"]
"""
Tests for the workouts application.
"""
from django.test import TestCase
# Create your tests here.
from django.urls import path, include
from diets import views
from rest_framework.urlpatterns import format_suffix_patterns
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
# This is a bit messy and will need to change
urlpatterns = format_suffix_patterns(
[
path("", views.api_root),
path("api/diets/", views.DietList.as_view(), name="diet-list"),
path(
"api/diets/<int:pk>/",
views.DietDetail.as_view(),
name="diet-detail",
),
path("api/mealtypes/", views.MealtypeList.as_view(), name="mealtype-list"),
path(
"api/mealtypes/<int:pk>/",
views.MealtypeDetail.as_view(),
name="mealtype-detail",
),
path(
"api/mealtype-instances/",
views.MealtypeInstanceList.as_view(),
name="mealtype-instance-list",
),
path(
"api/mealtype-instances/<int:pk>/",
views.MealtypeInstanceDetail.as_view(),
name="mealtypeinstance-detail",
),
path(
"api/diet-files/",
views.DietFileList.as_view(),
name="diet-file-list",
),
path(
"api/diet-files/<int:pk>/",
views.DietFileDetail.as_view(),
name="dietfile-detail",
),
path("", include("users.urls")),
path("", include("comments.urls")),
path("api/auth/", include("rest_framework.urls")),
path("api/token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("api/token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
path("api/remember_me/", views.RememberMe.as_view(), name="remember_me"),
]
)
"""Contains views for the diets application. These are mostly class-based views.
"""
from rest_framework import generics, mixins
from rest_framework import permissions
from rest_framework.parsers import (
JSONParser,
)
from rest_framework.decorators import api_view
from rest_framework.response import Response
from rest_framework.reverse import reverse
from django.db.models import Q
from rest_framework import filters
from diets.parsers import MultipartJsonParser
from diets.permissions import (
IsOwner,
IsCoachAndVisibleToCoach,
IsOwnerOfDiet,
IsCoachOfDietAndVisibleToCoach,
IsReadOnly,
IsPublic,
IsDietPublic,
)
from diets.mixins import CreateListModelMixin
from diets.models import Diet, Mealtype, MealtypeInstance, DietFile
from diets.serializers import DietSerializer, MealtypeSerializer
from diets.serializers import RememberMeSerializer
from diets.serializers import MealtypeInstanceSerializer, DietFileSerializer
from django.core.exceptions import PermissionDenied
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.response import Response
import json
from collections import namedtuple
import base64, pickle
from django.core.signing import Signer
@api_view(["GET"])
def api_root(request, format=None):
return Response(
{
"users": reverse("user-list", request=request, format=format),
"diets": reverse("diet-list", request=request, format=format),
"mealtypes": reverse("mealtype-list", request=request, format=format),
"mealtype-instances": reverse(
"mealtype-instance-list", request=request, format=format
),
"diet-files": reverse(
"diet-file-list", request=request, format=format
),
"comments": reverse("comment-list", request=request, format=format),
"likes": reverse("like-list", request=request, format=format),
}
)
# Allow users to save a persistent session in their browser
class RememberMe(
mixins.ListModelMixin,
mixins.CreateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
serializer_class = RememberMeSerializer
def get(self, request):
if request.user.is_authenticated == False:
raise PermissionDenied
else:
return Response({"remember_me": self.rememberme()})
def post(self, request):
cookieObject = namedtuple("Cookies", request.COOKIES.keys())(
*request.COOKIES.values()
)
user = self.get_user(cookieObject)
refresh = RefreshToken.for_user(user)
return Response(
{
"refresh": str(refresh),
"access": str(refresh.access_token),
}
)
def get_user(self, cookieObject):
decode = base64.b64decode(cookieObject.remember_me)
user, sign = pickle.loads(decode)
# Validate signature
if sign == self.sign_user(user):
return user
def rememberme(self):
creds = [self.request.user, self.sign_user(str(self.request.user))]
return base64.b64encode(pickle.dumps(creds))
def sign_user(self, username):
signer = Signer()
signed_user = signer.sign(username)
return signed_user
class DietList(
mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView
):
"""Class defining the web response for the creation of a Diet, or displaying a list
of Diets
HTTP methods: GET, POST
"""
serializer_class = DietSerializer
permission_classes = [
permissions.IsAuthenticated
] # User must be authenticated to create/view diets
parser_classes = [
MultipartJsonParser,
JSONParser,
] # For parsing JSON and Multi-part requests
filter_backends = [filters.OrderingFilter]
ordering_fields = ["name", "date", "owner__username"]
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def get_queryset(self):
qs = Diet.objects.none()
if self.request.user:
# A diet should be visible to the requesting user if any of the following hold:
# - The diet has public visibility
# - The owner of the diet is the requesting user
# - The diet has coach visibility and the requesting user is the owner's coach
qs = Diet.objects.filter(
Q(visibility="PU")
| (Q(visibility="CO") & Q(owner__coach=self.request.user))
).distinct()
return qs
class DietDetail(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
"""Class defining the web response for the details of an individual Diet.
HTTP methods: GET, PUT, DELETE
"""
queryset = Diet.objects.all()
serializer_class = DietSerializer
permission_classes = [
permissions.IsAuthenticated
& (IsOwner | (IsReadOnly & (IsCoachAndVisibleToCoach | IsPublic)))
]
parser_classes = [MultipartJsonParser, JSONParser]
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
class MealtypeList(
mixins.ListModelMixin, mixins.CreateModelMixin, generics.GenericAPIView
):
"""Class defining the web response for the creation of an Mealtype, or
a list of Mealtypes.
HTTP methods: GET, POST
"""
queryset = Mealtype.objects.all()
serializer_class = MealtypeSerializer
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
class MealtypeDetail(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
"""Class defining the web response for the details of an individual Mealtype.
HTTP methods: GET, PUT, PATCH, DELETE
"""
queryset = Mealtype.objects.all()
serializer_class = MealtypeSerializer
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
class MealtypeInstanceList(
mixins.ListModelMixin,
mixins.CreateModelMixin,
CreateListModelMixin,
generics.GenericAPIView,
):
"""Class defining the web response for the creation"""
serializer_class = MealtypeInstanceSerializer
permission_classes = [permissions.IsAuthenticated & IsOwnerOfDiet]
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def get_queryset(self):
qs = MealtypeInstance.objects.none()
if self.request.user:
qs = MealtypeInstance.objects.filter(
Q(diet__owner=self.request.user)
| (
(Q(diet__visibility="CO") | Q(diet__visibility="PU"))
& Q(diet__owner__coach=self.request.user)
)
).distinct()
return qs
class MealtypeInstanceDetail(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
serializer_class = MealtypeInstanceSerializer
permission_classes = [
permissions.IsAuthenticated
& (
IsOwnerOfDiet
| (IsReadOnly & (IsCoachOfDietAndVisibleToCoach | IsDietPublic))
)
]
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def patch(self, request, *args, **kwargs):
return self.partial_update(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
class DietFileList(
mixins.ListModelMixin,
mixins.CreateModelMixin,
CreateListModelMixin,
generics.GenericAPIView,
):
queryset = DietFile.objects.all()
serializer_class = DietFileSerializer
permission_classes = [permissions.IsAuthenticated & IsOwnerOfDiet]
parser_classes = [MultipartJsonParser, JSONParser]
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
def get_queryset(self):
qs = DietFile.objects.none()
if self.request.user:
qs = DietFile.objects.filter(
Q(owner=self.request.user)
| Q(diet__owner=self.request.user)
| (
Q(diet__visibility="CO")
& Q(diet__owner__coach=self.request.user)
)
).distinct()
return qs
class DietFileDetail(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
generics.GenericAPIView,
):
queryset = DietFile.objects.all()
serializer_class = DietFileSerializer
permission_classes = [
permissions.IsAuthenticated
& (
IsOwner
| IsOwnerOfDiet
| (IsReadOnly & (IsCoachOfDietAndVisibleToCoach | IsDietPublic))
)
]
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def delete(self, request, *args, **kwargs):
return self.destroy(request, *args, **kwargs)
......@@ -58,6 +58,7 @@ INSTALLED_APPS = [
"django.contrib.staticfiles",
"rest_framework",
"workouts.apps.WorkoutsConfig",
"diets.apps.DietsConfig",
"users.apps.UsersConfig",
"comments.apps.CommentsConfig",
"corsheaders",
......
......@@ -21,6 +21,7 @@ from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("workouts.urls")),
path("", include("diets.urls")),
]
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diet</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://kit.fontawesome.com/0ce6c392ca.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="styles/style.css">
<script src="scripts/navbar.js" type="text/javascript" defer></script>
</head>
<body>
<navbar-el></navbar-el>
<div class="container">
<div class="row">
<div class="col-lg">
<h3 class="mt-3">View/Edit Diet</h3>
</div>
</div>
<form class="row g-3 mb-4" id="form-diet">
<div class="col-lg-6 ">
<label for="inputName" class="form-label">Name</label>
<input type="text" class="form-control" id="inputName" name="name" readonly>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<label for="inputDateTime" class="form-label">Date/Time</label>
<input type="datetime-local" class="form-control" id="inputDateTime" name="date" readonly>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<label for="inputOwner" class="form-label">Owner</label>
<input type="text" class="form-control" id="inputOwner" name="owner_username" readonly>
</div>
<div class="col-lg-6">
<label for="inputVisibility" class="form-label">Visibility</label>
<select id="inputVisibility" class="form-select" name="visibility" disabled>
<option value="PU">Public</option>
<option value="CO">Coach</option>
<option value="PR">Private</option>
</select>
</div>
<div class="col-lg-6">
<label for="inputNotes" class="form-label">Notes</label>
<textarea class="form-control" id="inputNotes" name="notes" readonly></textarea>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<div class="input-group">
<input type="file" class="form-control" id="customFile" name="files" multiple disabled>
</div>
<div id="uploaded-files" class="ms-1 mt-2">
</div>
</div>
<div class="col-lg-6">
</div>
<div class="col-lg-6">
<input type="button" class="btn btn-primary hide" id="btn-ok-diet" value=" OK ">
<input type="button" class="btn btn-primary hide" id="btn-edit-diet" value=" Edit ">
<input type="button" class="btn btn-secondary hide" id="btn-cancel-diet" value="Cancel">
<input type="button" class="btn btn-danger float-end hide" id="btn-delete-diet" value="Delete">
</div>
<div class="col-lg-6"></div>
<div class="col-lg-12">
<h3 class="mt-3">Mealtypes</h3>
</div>
<div id="div-mealtypes" class="col-lg-12">
</div>
<div class="col-lg-6">
<input type="button" class="btn btn-primary hide" id="btn-add-mealtype" value="Add mealtype">
<input type="button" class="btn btn-danger hide" id="btn-remove-mealtype" value="Remove mealtype">
</div>
<div class="col-lg-6"></div>
</form>
<div class="row bootstrap snippets bootdeys" id="div-comment-row">
<div class="col-md-8 col-sm-12">
<div class="comment-wrapper">
<div class="card">
<div class="card-header bg-primary text-light">
Comment panel
</div>
<div class="card-body">
<textarea class="form-control" id="comment-area" placeholder="write a comment..." rows="3"></textarea>
<br>
<button type="button" id="post-comment" class="btn btn-info pull-right">Post</button>
<div class="clearfix"></div>
<hr>
<ul id="comment-list" class="list-unstyled">
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<template id="template-mealtype">
<div class="row div-mealtype-container g-3 mb-3">
<div class="col-lg-6"><h5>Mealtype</h5></div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<label class="form-label mealtype-type">Type</label>
<select class="form-select" name="type">
</select>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-3">
<label class="form-label mealtype-sets">Sets</label>
<input type="number" class="form-control" name="sets">
</div>
<div class="col-lg-3">
<label class="form-label mealtype-number">Number</label>
<input type="number" class="form-control" name="number">
</div>
<div class="col-lg-6"></div>
</div>
</template>
<script src="scripts/defaults.js"></script>
<script src="scripts/scripts.js"></script>
<script src="scripts/diet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Diets</title>
<link rel="stylesheet" href="styles/style.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://kit.fontawesome.com/0ce6c392ca.js" crossorigin="anonymous"></script>
<script src="scripts/navbar.js" type="text/javascript" defer></script>
</head>
<body>
<navbar-el></navbar-el>
<div class="container">
<div class="row">
<div class="col-lg text-center">
<h3 class="mt-5">View Diets</h3>
<p>Here you can view diets completed by you, your athletes,
or the public. Click on a diet to view its details.</p>
<input type="button" class="btn btn-success" id="btn-create-diet" value="Log new diet">
</div>
</div>
<div class="row">
<div class="col-lg text-center">
<div class="list-group list-group-horizontal d-inline-flex mt-2" id="list-tab" role="tablist">
<a class="list-group-item list-group-item-action active" id="list-all-diets-list" data-bs-toggle="list" href="#list-all-diets" role="tab" aria-controls="all">All Diets</a>
<a class="list-group-item list-group-item-action" id="list-my-diets-list" data-bs-toggle="list" href="#list-my-diets" role="tab" aria-controls="my">My Diets</a>
<a class="list-group-item list-group-item-action" id="list-athlete-diets-list" data-bs-toggle="list" href="#list-athlete-diets" role="tab" aria-controls="athlete">Athlete Diets</a>
<a class="list-group-item list-group-item-action" id="list-public-diets-list" data-bs-toggle="list" href="#list-public-diets" role="tab" aria-controls="public">Public Diets</a>
</div>
<div class="mt-1">Sort by: <a href="?ordering=date">Date</a> <a href="?ordering=owner">Owner</a> <a href="?ordering=name">Name</a>
<br>Currently sorting by: <span id="current-sort"></span>
</div>
<div class="list-group mt-1" id="div-content"></div>
</div>
</div>
</div>
<template id="template-diet">
<a class="list-group-item list-group-item-action flex-column align-items-start my-1 diet">
<div class="d-flex w-100 justify-content-between align-items-center">
<h5 class="mb-1"></h5>
</div>
<div class="d-flex">
<table class="mb-1 text-start">
<tr><td>Date:</td><td></td></tr>
<tr><td>Time:</td><td></td></tr>
<tr><td>Owner:</td><td></td></tr>
<tr><td>Mealtypes:</td><td></td></tr>
</table>
</div>
</a>
</template>
<script src="scripts/defaults.js"></script>
<script src="scripts/scripts.js"></script>
<script src="scripts/diets.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mealtype</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://kit.fontawesome.com/0ce6c392ca.js" crossorigin="anonymous"></script>
<link rel="stylesheet" href="styles/style.css">
<script src="scripts/navbar.js" type="text/javascript" defer></script>
</head>
<body>
<navbar-el></navbar-el>
<div class="container">
<div class="row">
<div class="col-lg">
<h3 class="mt-3">View/Edit Mealtype</h3>
</div>
</div>
<form class="row g-3" id="form-mealtype">
<div class="col-lg-6 ">
<label for="inputName" class="form-label">Name</label>
<input type="text" class="form-control" id="inputName" name="name" readonly>
</div>
<div class="col-lg-6"></div>
<div class="col-lg-6">
<label for="inputDescription" class="form-label">Description</label>
<textarea class="form-control" id="inputDescription" name="description" readonly></textarea>
</div>
<div class="col-lg-6"></div>
<!-- <div class="col-lg-6">
<label for="inputUnit" class="form-label">Unit</label>
<input type="text" class="form-control" id="inputUnit" name="unit" readonly>
</div> -->
<div class="col-lg-6"></div>
<div class="col-lg-6">
<input type="button" class="btn btn-primary hide" id="btn-ok-mealtype" value=" OK ">
<input type="button" class="btn btn-primary" id="btn-edit-mealtype" value=" Edit ">
<input type="button" class="btn btn-secondary hide" id="btn-cancel-mealtype" value="Cancel">
<input type="button" class="btn btn-danger float-end hide" id="btn-delete-mealtype" value="Delete">
</div>
<div class="col-lg-6">
</div>
</form>
</div>
<script src="scripts/defaults.js"></script>
<script src="scripts/scripts.js"></script>
<script src="scripts/mealtype.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment