diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5e51efe --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "intermediate_team_3", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/server/pyproject.toml b/server/pyproject.toml index d34292d..8850f7e 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -12,6 +12,7 @@ dependencies = [ "gunicorn~=23.0", "python-dotenv~=1.0", "psycopg[binary,pool]~=3.2.13", + "djangorestframework-simplejwt>=5.5.1", ] [dependency-groups] diff --git a/server/server/settings.py b/server/server/settings.py index af546bb..1e05ddf 100644 --- a/server/server/settings.py +++ b/server/server/settings.py @@ -48,6 +48,8 @@ "rest_framework", "corsheaders", "healthcheck", + "user_profile", + "task_planner", ] MIDDLEWARE = [ @@ -70,6 +72,13 @@ ROOT_URLCONF = "server.urls" +REST_FRAMEWORK = { + "DEFAULT_AUHTENTICATION_CLASSES": ( + "rest_framework.authenticaiton.SessionAuthenticaiton", + "rest_framework_simplejwt.authentication.JWTAuthenticaiton", + ) +} + TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", diff --git a/server/server/urls.py b/server/server/urls.py index dd710be..d9eaca5 100644 --- a/server/server/urls.py +++ b/server/server/urls.py @@ -18,7 +18,14 @@ from django.contrib import admin from django.urls import include, path +from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView + urlpatterns = [ path("admin/", admin.site.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/healthcheck/", include("healthcheck.urls")), + path("api/user/", include("user_profile.urls")), + path("api/planner/", include("task_planner.urls")), ] diff --git a/server/task_planner/__init__.py b/server/task_planner/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/task_planner/admin.py b/server/task_planner/admin.py new file mode 100644 index 0000000..ab61855 --- /dev/null +++ b/server/task_planner/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import Task, Time, Topic + +# Register your models here. +admin.site.register(Task) +admin.site.register(Time) +admin.site.register(Topic) diff --git a/server/task_planner/apps.py b/server/task_planner/apps.py new file mode 100644 index 0000000..46dffb7 --- /dev/null +++ b/server/task_planner/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TaskPlannerConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "task_planner" diff --git a/server/task_planner/migrations/0001_initial.py b/server/task_planner/migrations/0001_initial.py new file mode 100644 index 0000000..29f462e --- /dev/null +++ b/server/task_planner/migrations/0001_initial.py @@ -0,0 +1,95 @@ +# Generated by Django 5.2.8 on 2025-12-07 11:16 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Task", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True)), + ("completed", models.BooleanField(default=False)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Time", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("day", models.IntegerField()), + ("start_time", models.TimeField()), + ("end_time", models.TimeField()), + ("repeating", models.BooleanField(default=False)), + ( + "task", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="task", + to="task_planner.task", + ), + ), + ], + ), + migrations.CreateModel( + name="Topic", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ("color_hex", models.PositiveIntegerField()), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.AddField( + model_name="task", + name="topics", + field=models.ManyToManyField(to="task_planner.topic"), + ), + ] diff --git a/server/task_planner/migrations/__init__.py b/server/task_planner/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/task_planner/models.py b/server/task_planner/models.py new file mode 100644 index 0000000..6f214fc --- /dev/null +++ b/server/task_planner/models.py @@ -0,0 +1,29 @@ +from django.db import models +from django.contrib.auth import get_user_model + + +# Create your models here. +User = get_user_model() + + +class Topic(models.Model): + name = models.CharField(max_length=255) + color_hex = models.PositiveIntegerField() + user = models.ForeignKey(User, on_delete=models.CASCADE) + + +class Task(models.Model): + name = models.CharField(max_length=255) + description = models.TextField(blank=True) + completed = models.BooleanField(default=False) + user = models.ForeignKey(User, on_delete=models.CASCADE) + topics = models.ManyToManyField(Topic) + + +class Time(models.Model): + day = models.IntegerField() + start_time = models.TimeField() + end_time = models.TimeField() + repeating = models.BooleanField(default=False) + task = models.ForeignKey( + Task, related_name="times", on_delete=models.CASCADE) diff --git a/server/task_planner/serializers.py b/server/task_planner/serializers.py new file mode 100644 index 0000000..0203d07 --- /dev/null +++ b/server/task_planner/serializers.py @@ -0,0 +1,31 @@ +from rest_framework import serializers +from .models import Task, Topic, Time + + +class TopicSerializer(serializers.ModelSerializer): + class Meta: + model = Topic + fields = "__all__" + + +class TimeSerializer(serializers.ModelSerializer): + class Meta: + model = Time + fields = "__all__" + + +class TaskSerializer(serializers.ModelSerializer): + topics = TopicSerializer(many=True, read_only=True) + times = TimeSerializer(many=True, read_only=True) + + class Meta: + model = Task + fields = ( + "id", + "name", + "description", + "completed", + "user", + "topics", + "times", + ) diff --git a/server/task_planner/tests.py b/server/task_planner/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/task_planner/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/task_planner/urls.py b/server/task_planner/urls.py new file mode 100644 index 0000000..070e600 --- /dev/null +++ b/server/task_planner/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +app_name = "task_planner" +urlpatterns = [ + path("task/", views.TaskList.as_view(), name="task-list"), + path("topic/", views.TopicList.as_view(), name="topic-list"), + path("time/", views.TimeList.as_view(), name="time-list"), +] diff --git a/server/task_planner/views.py b/server/task_planner/views.py new file mode 100644 index 0000000..f679995 --- /dev/null +++ b/server/task_planner/views.py @@ -0,0 +1,30 @@ +from django.shortcuts import render + +# from rest_framework import generics, permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from .models import Task, Topic, Time +from .serializers import TaskSerializer, TopicSerializer, TimeSerializer + + +# Create your views here. +class TaskList(APIView): + def get(self, request): + tasks = Task.objects.all() + serializer = TaskSerializer(tasks, many=True) + return Response(serializer.data) + + +class TopicList(APIView): + def get(self, request): + topics = Topic.objects.all() + serializer = TopicSerializer(topics, many=True) + return Response(serializer.data) + + +class TimeList(APIView): + def get(self, request): + times = Time.objects.all() + serializer = TimeSerializer(times, many=True) + return Response(serializer.data) diff --git a/server/user_profile/__init__.py b/server/user_profile/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/user_profile/admin.py b/server/user_profile/admin.py new file mode 100644 index 0000000..4a13cdd --- /dev/null +++ b/server/user_profile/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from .models import Profile + +# Register your models here. +admin.site.register(Profile) diff --git a/server/user_profile/apps.py b/server/user_profile/apps.py new file mode 100644 index 0000000..fe0436b --- /dev/null +++ b/server/user_profile/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserProfileConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'user_profile' diff --git a/server/user_profile/migrations/0001_initial.py b/server/user_profile/migrations/0001_initial.py new file mode 100644 index 0000000..4427334 --- /dev/null +++ b/server/user_profile/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 5.2.8 on 2025-11-29 06:33 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('bio', models.TextField()), + ('age', models.PositiveIntegerField(null=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/server/user_profile/migrations/0002_alter_profile_age_alter_profile_bio.py b/server/user_profile/migrations/0002_alter_profile_age_alter_profile_bio.py new file mode 100644 index 0000000..5e7c1ab --- /dev/null +++ b/server/user_profile/migrations/0002_alter_profile_age_alter_profile_bio.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.8 on 2025-12-02 03:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("user_profile", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="age", + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AlterField( + model_name="profile", + name="bio", + field=models.TextField(blank=True), + ), + ] diff --git a/server/user_profile/migrations/__init__.py b/server/user_profile/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/user_profile/models.py b/server/user_profile/models.py new file mode 100644 index 0000000..e7d5043 --- /dev/null +++ b/server/user_profile/models.py @@ -0,0 +1,21 @@ +from django.db import models +from django.contrib.auth import get_user_model +from django.db.models.signals import post_save +from django.dispatch import receiver + +# Create your models here. +User = get_user_model() + + +class Profile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE) + bio = models.TextField(blank=True) + age = models.PositiveIntegerField(null=True, blank=True) + + def __str__(self): + return f"Profile: {self.user.username}" + + +@receiver(post_save, sender=User) +def create_or_update_user_profile(sender, instance, created, **kwargs): + Profile.objects.get_or_create(user=instance) \ No newline at end of file diff --git a/server/user_profile/permissions.py b/server/user_profile/permissions.py new file mode 100644 index 0000000..ec46bbb --- /dev/null +++ b/server/user_profile/permissions.py @@ -0,0 +1,12 @@ +from rest_framework import permissions +from django.contrib.auth import get_user_model + +User = get_user_model() + +class IsUserOrReadOnly(permissions.BasePermission): + def has_object_permission(self, request, view, obj): + if isinstance(obj, User): + # If this is for the user object check against logged in user + return obj == request.user + # Otherwise check user field against logged in user + return obj.user == request.user \ No newline at end of file diff --git a/server/user_profile/serializers.py b/server/user_profile/serializers.py new file mode 100644 index 0000000..512cdc4 --- /dev/null +++ b/server/user_profile/serializers.py @@ -0,0 +1,25 @@ +from rest_framework import serializers +from .models import Profile, User + + +class ProfileSerializer(serializers.ModelSerializer): + class Meta: + model = Profile + fields = "__all__" + + +class UserSerializer(serializers.ModelSerializer): + profile = ProfileSerializer(read_only=True) + + class Meta: + model = User + fields = ( + "id", + "username", + "email", + "first_name", + "last_name", + "is_staff", + "is_superuser", + "profile", + ) diff --git a/server/user_profile/tests.py b/server/user_profile/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/server/user_profile/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/server/user_profile/urls.py b/server/user_profile/urls.py new file mode 100644 index 0000000..f14400c --- /dev/null +++ b/server/user_profile/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +app_name = "user_profiles" +urlpatterns = [ + path("profile//", views.UserProfileDetail.as_view(), name="user-list"), + path("profile/", views.UserProfileList.as_view(), name="profile-list"), + path("", views.UserList.as_view(), name="list"), +] diff --git a/server/user_profile/views.py b/server/user_profile/views.py new file mode 100644 index 0000000..8c60f45 --- /dev/null +++ b/server/user_profile/views.py @@ -0,0 +1,29 @@ +from django.shortcuts import render + +from rest_framework import generics, permissions +from rest_framework.response import Response +from rest_framework.views import APIView + +from .models import User, Profile +from .permissions import IsUserOrReadOnly +from .serializers import UserSerializer, ProfileSerializer + +# Create your views here. +class UserList(APIView): + permission_classes = (permissions.IsAuthenticated,) + + def get(self, request): + users = User.objects.all() + serializer = UserSerializer(users, many=True) + return Response(serializer.data) + + +class UserProfileList(generics.ListAPIView): + queryset = Profile.objects.all() + serializer_class = ProfileSerializer + + +class UserProfileDetail(generics.RetrieveUpdateAPIView): + permissions_classes = (permissions.IsAuthenticated, IsUserOrReadOnly) + queryset = Profile.objects.all() + serializer_class = ProfileSerializer diff --git a/server/uv.lock b/server/uv.lock index 6a7af8c..d13fcab 100644 --- a/server/uv.lock +++ b/server/uv.lock @@ -120,6 +120,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/ce/bf8b9d3f415be4ac5588545b5fcdbbb841977db1c1d923f7568eeabe1689/djangorestframework-3.16.1-py3-none-any.whl", hash = "sha256:33a59f47fb9c85ede792cbf88bde71893bcda0667bc573f784649521f1102cec", size = 1080442, upload-time = "2025-08-06T17:50:50.667Z" }, ] +[[package]] +name = "djangorestframework-simplejwt" +version = "5.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "django" }, + { name = "djangorestframework" }, + { name = "pyjwt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/27/2874a325c11112066139769f7794afae238a07ce6adf96259f08fd37a9d7/djangorestframework_simplejwt-5.5.1.tar.gz", hash = "sha256:e72c5572f51d7803021288e2057afcbd03f17fe11d484096f40a460abc76e87f", size = 101265, upload-time = "2025-07-21T16:52:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/94/fdfb7b2f0b16cd3ed4d4171c55c1c07a2d1e3b106c5978c8ad0c15b4a48b/djangorestframework_simplejwt-5.5.1-py3-none-any.whl", hash = "sha256:2c30f3707053d384e9f315d11c2daccfcb548d4faa453111ca19a542b732e469", size = 107674, upload-time = "2025-07-21T16:52:07.493Z" }, +] + [[package]] name = "gunicorn" version = "23.0.0" @@ -225,6 +239,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + [[package]] name = "pytest" version = "8.4.2" @@ -284,6 +307,7 @@ dependencies = [ { name = "django" }, { name = "django-cors-headers" }, { name = "djangorestframework" }, + { name = "djangorestframework-simplejwt" }, { name = "gunicorn" }, { name = "psycopg", extra = ["binary", "pool"] }, { name = "python-dotenv" }, @@ -301,6 +325,7 @@ requires-dist = [ { name = "django", specifier = "~=5.2" }, { name = "django-cors-headers", specifier = "~=4.9" }, { name = "djangorestframework", specifier = "~=3.16" }, + { name = "djangorestframework-simplejwt", specifier = ">=5.5.1" }, { name = "gunicorn", specifier = "~=23.0" }, { name = "psycopg", extras = ["binary", "pool"], specifier = "~=3.2.13" }, { name = "python-dotenv", specifier = "~=1.0" },