Compare commits
17 commits
main
...
feature/in
Author | SHA1 | Date | |
---|---|---|---|
ca2d50e26b | |||
36da9c4dd0 | |||
17b1363ee4 | |||
cdd4a01521 | |||
f7a631d543 | |||
0739545a41 | |||
a103ac22f5 | |||
398a2c7c08 | |||
864a7741e6 | |||
55fc73efe7 | |||
f3daf83cb9 | |||
63cb2d7a8e | |||
827508a480 | |||
b614d8fb39 | |||
6da4073ced | |||
d45f1f1f89 | |||
e33ed56963 |
36 changed files with 422 additions and 15 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -396,9 +396,4 @@ pyrightconfig.json
|
|||
.ionide
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/django,pycharm,python,visualstudiocode
|
||||
/.idea/inspectionProfiles/profiles_settings.xml
|
||||
/.idea/.gitignore
|
||||
/.idea/misc.xml
|
||||
/.idea/mixr-api.iml
|
||||
/.idea/modules.xml
|
||||
/.idea/vcs.xml
|
||||
/.idea
|
9
README.md
Normal file
9
README.md
Normal 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
0
api_v1/__init__.py
Normal file
14
api_v1/admin.py
Normal file
14
api_v1/admin.py
Normal 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
6
api_v1/apps.py
Normal 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
0
api_v1/auth/__init__.py
Normal file
25
api_v1/auth/registration_view.py
Normal file
25
api_v1/auth/registration_view.py
Normal 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")
|
0
api_v1/cafeteria/__init__.py
Normal file
0
api_v1/cafeteria/__init__.py
Normal file
19
api_v1/cafeteria/model.py
Normal file
19
api_v1/cafeteria/model.py
Normal 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)
|
0
api_v1/institution/__init__.py
Normal file
0
api_v1/institution/__init__.py
Normal file
22
api_v1/institution/model.py
Normal file
22
api_v1/institution/model.py
Normal 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
|
0
api_v1/location/__init__.py
Normal file
0
api_v1/location/__init__.py
Normal file
22
api_v1/location/model.py
Normal file
22
api_v1/location/model.py
Normal 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)
|
24
api_v1/migrations/0001_createBasicUserInformation.py
Normal file
24
api_v1/migrations/0001_createBasicUserInformation.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
19
api_v1/migrations/0002_rebrandUserInformationToProfile.py
Normal file
19
api_v1/migrations/0002_rebrandUserInformationToProfile.py
Normal 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',
|
||||
),
|
||||
]
|
28
api_v1/migrations/0003_createdLocationModel.py
Normal file
28
api_v1/migrations/0003_createdLocationModel.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
24
api_v1/migrations/0004_createdCafeteriaModel.py
Normal file
24
api_v1/migrations/0004_createdCafeteriaModel.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
30
api_v1/migrations/0005_addedInstitutionModel.py
Normal file
30
api_v1/migrations/0005_addedInstitutionModel.py
Normal 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'),
|
||||
),
|
||||
]
|
27
api_v1/migrations/0006_addedMoodModel.py
Normal file
27
api_v1/migrations/0006_addedMoodModel.py
Normal 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)),
|
||||
],
|
||||
),
|
||||
]
|
0
api_v1/migrations/__init__.py
Normal file
0
api_v1/migrations/__init__.py
Normal file
5
api_v1/models.py
Normal file
5
api_v1/models.py
Normal 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
0
api_v1/mood/__init__.py
Normal file
20
api_v1/mood/model.py
Normal file
20
api_v1/mood/model.py
Normal 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
|
0
api_v1/profile/__init__.py
Normal file
0
api_v1/profile/__init__.py
Normal file
11
api_v1/profile/model.py
Normal file
11
api_v1/profile/model.py
Normal 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)
|
48
api_v1/profile/profile_services.py
Normal file
48
api_v1/profile/profile_services.py
Normal 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
|
0
api_v1/schedule/__init__.py
Normal file
0
api_v1/schedule/__init__.py
Normal file
13
api_v1/schedule/model.py
Normal file
13
api_v1/schedule/model.py
Normal 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
0
api_v1/slot/__init__.py
Normal file
34
api_v1/slot/model.py
Normal file
34
api_v1/slot/model.py
Normal 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
3
api_v1/tests.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
5
api_v1/urls.py
Normal file
5
api_v1/urls.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.urls import path, include
|
||||
|
||||
# Assign register url
|
||||
urlpatterns = [
|
||||
]
|
3
api_v1/views.py
Normal file
3
api_v1/views.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
|
@ -15,7 +15,6 @@ 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/5.0/howto/deployment/checklist/
|
||||
|
||||
|
@ -27,7 +26,6 @@ DEBUG = True
|
|||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
@ -37,6 +35,10 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'api_v1.apps.ApiV1Config',
|
||||
'rest_framework',
|
||||
'colorfield',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -69,7 +71,6 @@ TEMPLATES = [
|
|||
|
||||
WSGI_APPLICATION = 'mixr_api.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
|
||||
|
||||
|
@ -84,7 +85,6 @@ DATABASES = {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||
|
||||
|
@ -103,7 +103,6 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||
|
||||
|
@ -115,7 +114,6 @@ USE_I18N = True
|
|||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||
|
||||
|
|
|
@ -15,8 +15,9 @@ Including another URLconf
|
|||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path("v1/", include("api_v1.urls")),
|
||||
]
|
||||
|
|
|
@ -1,2 +1,4 @@
|
|||
Django~=5.0.2
|
||||
Django~=4.2
|
||||
psycopg2-binary~=2.9.9
|
||||
djangorestframework~=3.14.0
|
||||
django-colorfield~=0.11.0
|
Loading…
Reference in a new issue