Compare commits

...

17 commits

Author SHA1 Message Date
ca2d50e26b Some prototyping the registration 2024-02-29 00:06:45 +01:00
36da9c4dd0 Prototypes of Schedule and Slot model 2024-02-29 00:06:16 +01:00
17b1363ee4 Added Mood model 2024-02-29 00:05:41 +01:00
cdd4a01521 Added models for Cafeteria, Location, Institution 2024-02-28 21:26:34 +01:00
f7a631d543 Rebranded UserInformation to Profile 2024-02-28 20:43:45 +01:00
0739545a41 Adding rest framework to dependencies 2024-02-28 20:22:15 +01:00
Max
a103ac22f5 Working on registration endpoint and authentication in general 2024-02-28 17:33:16 +01:00
Max
398a2c7c08 Following django async style-guide for user services 2024-02-28 14:00:53 +01:00
Max
864a7741e6 Made user services async 2024-02-28 13:56:38 +01:00
Max
55fc73efe7 Properly added .idea folder to gitignore 2024-02-28 13:46:50 +01:00
Max
f3daf83cb9 Created basic User Information and user services 2024-02-28 13:45:21 +01:00
63cb2d7a8e Package init 2024-02-25 19:17:24 +01:00
827508a480 Created api v1 app 2024-02-25 19:16:55 +01:00
b614d8fb39 Added some database setup infos 2024-02-25 18:47:10 +01:00
6da4073ced Merge remote-tracking branch 'origin/feature/initial_models' into feature/initial_models 2024-02-25 18:41:39 +01:00
d45f1f1f89 Some initial user model setup 2024-02-25 18:39:37 +01:00
e33ed56963 Some initial user model setup 2024-02-25 00:32:15 +01:00
36 changed files with 422 additions and 15 deletions

7
.gitignore vendored
View file

@ -396,9 +396,4 @@ pyrightconfig.json
.ionide .ionide
# End of https://www.toptal.com/developers/gitignore/api/django,pycharm,python,visualstudiocode # End of https://www.toptal.com/developers/gitignore/api/django,pycharm,python,visualstudiocode
/.idea/inspectionProfiles/profiles_settings.xml /.idea
/.idea/.gitignore
/.idea/misc.xml
/.idea/mixr-api.iml
/.idea/modules.xml
/.idea/vcs.xml

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# Mixr API
## Setup
### Install requirements
If there are any issues with `psycopg2` and that it cannot be imported, you might be missing `libpq-dev`. If you are using a virtual environment, install it with `sudo apt-get install libpq-dev`.
If there are any issues with `djangorestframework` or rather the import of `rest_framework`, try installing it again with the active environment using `python -m pip install djangorestframework`.

0
api_v1/__init__.py Normal file
View file

14
api_v1/admin.py Normal file
View file

@ -0,0 +1,14 @@
from django.contrib import admin
from api_v1.cafeteria.model import Cafeteria
from api_v1.institution.model import Institution
from api_v1.location.model import Location
from api_v1.mood.model import Mood
from api_v1.profile.model import Profile
# Register your models here.
admin.site.register(Profile)
admin.site.register(Location)
admin.site.register(Cafeteria)
admin.site.register(Institution)
admin.site.register(Mood)

6
api_v1/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApiV1Config(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'api_v1'

0
api_v1/auth/__init__.py Normal file
View file

View file

@ -0,0 +1,25 @@
from django.views import View
from django.contrib.auth.models import User
from django.http import HttpResponse
from api_v1.profile.profile_services import acreate_profile
class RegistrationView(View):
async def post(self, request):
# Read parameters from body
username = request.POST.get('username')
password = request.POST.get('password')
email = request.POST.get('email')
display_name = request.POST.get('display_name')
# Check if profile already exists
if User.objects.filter(username=username).exists():
return HttpResponse(status=400, content="User already exists")
# Create profile
user = await User.objects.acreate_user(username, email, password)
# Create UserInformation
await acreate_profile(user, display_name=display_name)
return HttpResponse(status=201, content="User created")

View file

19
api_v1/cafeteria/model.py Normal file
View file

@ -0,0 +1,19 @@
import uuid
from django.db import models
from ..institution.model import Institution
from ..location.model import Location
class Cafeteria(models.Model):
cafeteria_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=50, blank=False, null=False)
location = models.OneToOneField(Location, on_delete=models.DO_NOTHING)
website = models.URLField()
institutes = models.ManyToManyField(Institution)
def __str__(self):
return '{} ({})'.format(self.name, self.location)

View file

View file

@ -0,0 +1,22 @@
import uuid
from django.db import models
from api_v1.location.model import Location
class Institution(models.Model):
institution_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=150, blank=False, null=False)
description = models.TextField(null=True)
location = models.OneToOneField(Location, on_delete=models.DO_NOTHING, null=True, blank=True)
website = models.URLField(null=True)
def __str__(self):
result = self.name
if self.description:
result += f' - {self.description}'
return result

View file

22
api_v1/location/model.py Normal file
View file

@ -0,0 +1,22 @@
import uuid
from django.db import models
class Location(models.Model):
location_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
longitude = models.DecimalField(max_digits=9, decimal_places=6)
latitude = models.DecimalField(max_digits=9, decimal_places=6)
country_code = models.CharField(max_length=2) # Following ISO-3166-1
country_name = models.CharField(max_length=50)
city = models.CharField(max_length=50)
postal_code = models.CharField(max_length=20)
street = models.CharField(max_length=50)
number = models.CharField(max_length=20)
def __str__(self):
return '{} {}, {} {}, {} {}'.format(self.street, self.number, self.postal_code, self.city, self.country_code,
self.country_name)

View file

@ -0,0 +1,24 @@
# Generated by Django 5.0.2 on 2024-02-28 12:44
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
]
operations = [
migrations.CreateModel(
name='UserInformation',
fields=[
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
('display_name', models.CharField(max_length=150)),
],
),
]

View file

@ -0,0 +1,19 @@
# Generated by Django 5.0.2 on 2024-02-28 19:41
from django.conf import settings
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('api_v1', '0001_createBasicUserInformation'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RenameModel(
old_name='UserInformation',
new_name='Profile',
),
]

View file

@ -0,0 +1,28 @@
# Generated by Django 5.0.2 on 2024-02-28 20:03
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api_v1', '0002_rebrandUserInformationToProfile'),
]
operations = [
migrations.CreateModel(
name='Location',
fields=[
('location_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('longitude', models.DecimalField(decimal_places=6, max_digits=9)),
('latitude', models.DecimalField(decimal_places=6, max_digits=9)),
('country_code', models.CharField(max_length=2)),
('country_name', models.CharField(max_length=50)),
('city', models.CharField(max_length=50)),
('postal_code', models.CharField(max_length=20)),
('street', models.CharField(max_length=50)),
('number', models.CharField(max_length=20)),
],
),
]

View file

@ -0,0 +1,24 @@
# Generated by Django 5.0.2 on 2024-02-28 20:03
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api_v1', '0003_createdLocationModel'),
]
operations = [
migrations.CreateModel(
name='Cafeteria',
fields=[
('cafeteria_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=50)),
('website', models.URLField()),
('location', models.OneToOneField(on_delete=django.db.models.deletion.DO_NOTHING, to='api_v1.location')),
],
),
]

View file

@ -0,0 +1,30 @@
# Generated by Django 5.0.2 on 2024-02-28 20:23
import django.db.models.deletion
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api_v1', '0004_createdCafeteriaModel'),
]
operations = [
migrations.CreateModel(
name='Institution',
fields=[
('institution_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=150)),
('description', models.TextField(null=True)),
('website', models.URLField(null=True)),
('location', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.DO_NOTHING, to='api_v1.location')),
],
),
migrations.AddField(
model_name='cafeteria',
name='institutes',
field=models.ManyToManyField(to='api_v1.institution'),
),
]

View file

@ -0,0 +1,27 @@
# Generated by Django 5.0.2 on 2024-02-28 22:55
import colorfield.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api_v1', '0005_addedInstitutionModel'),
]
operations = [
migrations.CreateModel(
name='Mood',
fields=[
('mood_id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('name', models.CharField(max_length=30)),
('description', models.CharField(max_length=150)),
('color', colorfield.fields.ColorField(default='#FFFFFF', image_field=None, max_length=25, samples=None)),
('is_available', models.BooleanField(default=True)),
('is_reliable', models.BooleanField(default=True)),
('is_going', models.BooleanField(default=False)),
],
),
]

View file

5
api_v1/models.py Normal file
View file

@ -0,0 +1,5 @@
from api_v1.profile.model import Profile
from api_v1.location.model import Location
from api_v1.cafeteria.model import Cafeteria
from api_v1.institution.model import Institution
from api_v1.mood.model import Mood

0
api_v1/mood/__init__.py Normal file
View file

20
api_v1/mood/model.py Normal file
View file

@ -0,0 +1,20 @@
import uuid
from django.db import models
from colorfield.fields import ColorField
class Mood(models.Model):
mood_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=30)
description = models.CharField(max_length=150, blank=False, null=False)
color = ColorField(format='hex')
is_available = models.BooleanField(default=True) # Is available to go
is_reliable = models.BooleanField(default=True) # Will always do the same in this mood
is_going = models.BooleanField(default=False) # Is going to the cafeteria
def __str__(self):
return self.name

View file

11
api_v1/profile/model.py Normal file
View file

@ -0,0 +1,11 @@
from django.contrib.auth.models import User
from django.db import models
class Profile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
display_name = models.CharField(max_length=150, null=False)
def __str__(self):
return '{} ({})'.format(self.display_name, self.user)

View file

@ -0,0 +1,48 @@
from api_v1.profile.model import Profile
from django.contrib.auth.models import User
from typing import Optional
async def acreate_profile(user: User, *, display_name: Optional[str] = None) -> Profile:
"""Async. Create a Profile object for the given profile.
@param user: The profile to create the Profile object for.
@param display_name: The display name for the profile. If None, the profile's username will be used.
@return: The created Profile object.
"""
profile = await Profile.objects.acreate(user=user, display_name=user.username)
# Use the given display name if provided.
if display_name is not None:
profile.display_name = display_name
await profile.asave()
return profile
async def aget_profile(user: User) -> Profile:
"""Async. Get the Profile object for the given profile.
@param user: The profile to get the Profile object for.
@return: The Profile object for the given profile.
"""
return await Profile.objects.aget(user)
async def aset_user_display_name(user: User, display_name: Optional[str] = None) -> Profile:
"""Async. Set the display name for the given profile.
@param user: The profile to set the display name for.
@param display_name: The display name to set for the profile. If None, the profile's username will be used.
@return: The Profile object for the given profile.
"""
profile = await Profile.objects.aget(user)
# Set the display name to the given display name, or the profile's username if None.
if display_name is not None:
profile.display_name = display_name
else:
profile.display_name = user.username
await profile.asave()
return profile

View file

13
api_v1/schedule/model.py Normal file
View file

@ -0,0 +1,13 @@
import uuid
from django.db import models
from api_v1.profile.model import Profile
class Schedule(models.Model):
schedule_id = models.UUIDField(primary_key=True, default=uuid.uuid4)
profile = models.ForeignKey(Profile, on_delete=models.DO_NOTHING, null=False)
slots = models.ManyToOneRel()

0
api_v1/slot/__init__.py Normal file
View file

34
api_v1/slot/model.py Normal file
View file

@ -0,0 +1,34 @@
import uuid
from django.core.exceptions import ValidationError
from django.db import models
def validate_weekday(value: int):
if value < 0 or value > 6:
raise ValidationError("Weekday must be between 0 (Mo) and 6 (Su)")
class Slot(models.Model):
slot_id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
start_time = models.TimeField()
end_time = models.TimeField()
duration = models.DurationField()
start_weekday = models.SmallIntegerField(validators=[validate_weekday])
end_weekday = models.SmallIntegerField(validators=[validate_weekday])
def __str__(self):
days = [
"Mo",
"Tu",
"We",
"Th",
"Fr",
"Sa",
"Su"
]
return "[{}] {} - [{}] {}".format(days[self.start_weekday], self.start_time, days[self.end_weekday],
self.end_time)

3
api_v1/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

5
api_v1/urls.py Normal file
View file

@ -0,0 +1,5 @@
from django.urls import path, include
# Assign register url
urlpatterns = [
]

3
api_v1/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

@ -15,7 +15,6 @@ from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'. # Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production # Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
@ -27,7 +26,6 @@ DEBUG = True
ALLOWED_HOSTS = [] ALLOWED_HOSTS = []
# Application definition # Application definition
INSTALLED_APPS = [ INSTALLED_APPS = [
@ -37,6 +35,10 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'api_v1.apps.ApiV1Config',
'rest_framework',
'colorfield',
] ]
MIDDLEWARE = [ MIDDLEWARE = [
@ -69,7 +71,6 @@ TEMPLATES = [
WSGI_APPLICATION = 'mixr_api.wsgi.application' WSGI_APPLICATION = 'mixr_api.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases # https://docs.djangoproject.com/en/5.0/ref/settings/#databases
@ -84,7 +85,6 @@ DATABASES = {
} }
} }
# Password validation # Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
@ -103,7 +103,6 @@ AUTH_PASSWORD_VALIDATORS = [
}, },
] ]
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/ # https://docs.djangoproject.com/en/5.0/topics/i18n/
@ -115,7 +114,6 @@ USE_I18N = True
USE_TZ = True USE_TZ = True
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/ # https://docs.djangoproject.com/en/5.0/howto/static-files/

View file

@ -15,8 +15,9 @@ Including another URLconf
2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
""" """
from django.contrib import admin from django.contrib import admin
from django.urls import path from django.urls import path, include
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path("v1/", include("api_v1.urls")),
] ]

View file

@ -1,2 +1,4 @@
Django~=5.0.2 Django~=4.2
psycopg2-binary~=2.9.9 psycopg2-binary~=2.9.9
djangorestframework~=3.14.0
django-colorfield~=0.11.0