diff --git a/caremyway/api/admin.py b/caremyway/api/admin.py index 8c38f3f..c775787 100644 --- a/caremyway/api/admin.py +++ b/caremyway/api/admin.py @@ -1,3 +1,11 @@ from django.contrib import admin +from .models import UserInfo, Client, Provider, WorkType, Manage, Price, Shift # Register your models here. +admin.site.register(UserInfo) +admin.site.register(Client) +admin.site.register(Provider) +admin.site.register(WorkType) +admin.site.register(Manage) +admin.site.register(Price) +admin.site.register(Shift) diff --git a/caremyway/api/models.py b/caremyway/api/models.py index d89ad4a..78446f5 100644 --- a/caremyway/api/models.py +++ b/caremyway/api/models.py @@ -6,39 +6,59 @@ from django.core.validators import RegexValidator class UserInfo(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) phone_regex = RegexValidator(regex=r'^\+?1?\d{9,15}$', message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.") - phone_number = models.CharField(validators=[phone_regex], max_length=16, blank=True) - USER_TYPES = (('C', 'Client'), ('P', 'Provider')) - user_type = models.CharField(max_length=1, choices=USER_TYPES, blank=True) + phone_number = models.CharField(validators=[phone_regex], max_length=16) + + def __str__(self): + return 'UserInfo: ' + self.user.username class Client(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - business_number = models.CharField(max_length=16, blank=True) + business_number = models.CharField(max_length=16) + + def __str__(self): + return 'Client: ' + self.user.username class Provider(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) - sin = models.CharField(max_length=16, blank=True) + sin = models.CharField(max_length=16) + + def __str__(self): + return 'Provider: ' + self.user.username -class Work(models.Model): - client = models.ForeignKey(Client, on_delete=models.CASCADE) - color = models.CharField(max_length=16, blank=True) - label = models.CharField(max_length=100, blank=True) +class WorkType(models.Model): + client = models.ForeignKey(Client, related_name='work_types', on_delete=models.CASCADE) + color = models.CharField(max_length=16) + label = models.CharField(max_length=100) + deleted = models.BooleanField(default=False) + + def __str__(self): + return 'WorkType: ' + self.label + ' of ' + self.client.user.username class Manage(models.Model): - client = models.ForeignKey(Client, on_delete=models.CASCADE) - provider = models.ForeignKey(Provider, on_delete=models.CASCADE) + client = models.ForeignKey(Client, related_name='employees', on_delete=models.CASCADE) + provider = models.ForeignKey(Provider, related_name='employers', on_delete=models.CASCADE) note = models.CharField(max_length=500, blank=True) + approved = models.NullBooleanField(blank=True) + deleted = models.BooleanField(default=False) + + def __str__(self): + return self.client.user.username + ' manages ' + self.provider.user.username class Price(models.Model): - client = models.ForeignKey(Client, on_delete=models.CASCADE) - provider = models.ForeignKey(Provider, on_delete=models.CASCADE) - work = models.ForeignKey(Work, on_delete=models.CASCADE) + management = models.ForeignKey(Manage, related_name='prices', on_delete=models.CASCADE) + work_type = models.ForeignKey(WorkType, on_delete=models.CASCADE) amount = models.DecimalField(max_digits=8, decimal_places=2) + deleted = models.BooleanField(default=False) class Shift(models.Model): - client = models.ForeignKey(Client, on_delete=models.CASCADE) - provider = models.ForeignKey(Provider, on_delete=models.CASCADE) - work = models.ForeignKey(Work, on_delete=models.CASCADE) + price = models.ForeignKey(Price, on_delete=models.CASCADE) + set_price = models.DecimalField(max_digits=8, decimal_places=2, null=True) + set_date = models.DateField() set_start = models.DateTimeField() set_end = models.DateTimeField() - amount = models.DecimalField(max_digits=8, decimal_places=2) - description = models.CharField(max_length=100, blank=True) + actual_start = models.DateTimeField(null=True) + actual_end = models.DateTimeField(null=True) + amount = models.DecimalField(max_digits=8, decimal_places=2, null=True) + description = models.CharField(max_length=100, null=True) + chart = models.TextField(max_length=1000, null=True) + deleted = models.BooleanField(default=False) diff --git a/caremyway/api/permissions.py b/caremyway/api/permissions.py index 61bb6af..4d2bd49 100644 --- a/caremyway/api/permissions.py +++ b/caremyway/api/permissions.py @@ -1,20 +1,3 @@ from pprint import pprint from rest_framework import permissions from caremyway.api.models import Client, Provider - -class UserTypePermission(permissions.BasePermission): - """ - Disallow creation of client or provider if one already exists - """ - message = "User is already either a client or a provider." - - def has_permission(self, request, view): - user = request.data.get('user', None) - - if request.method != 'POST' or user is None: - return True - elif not Client.objects.filter(user=user).exists() \ - and not Provider.objects.filter(user=user).exists(): - return True - else: - return False diff --git a/caremyway/api/serializers.py b/caremyway/api/serializers.py index 410e9d5..95514f8 100644 --- a/caremyway/api/serializers.py +++ b/caremyway/api/serializers.py @@ -1,32 +1,255 @@ +from datetime import datetime, timedelta, timezone +from decimal import Decimal from django.contrib.auth.models import User, Group +from django.shortcuts import get_object_or_404 from rest_framework import serializers -from caremyway.api.models import UserInfo, Client, Provider +from caremyway.api.models import UserInfo, Client, Provider, WorkType, Manage, Price, Shift class UserInfoSerializer(serializers.ModelSerializer): class Meta: model = UserInfo - fields = ('user', 'user_type', 'phone_number') + fields = ('phone_number',) -class ChosenUserInfoSerializer(UserInfoSerializer): - class Meta(UserInfoSerializer.Meta): - read_only_fields = ('user_type',) + def create(self, validated_data): + validated_data['user'] = self.context['request'].user + return serializers.ModelSerializer.create(self, validated_data) + +class WorkTypeSerializer(serializers.ModelSerializer): + class Meta: + model = WorkType + fields = ('id', 'color', 'label', 'deleted') + read_only_fields =('deleted',) + + def create(self, validated_data): + validated_data['client'] = self.context['request'].user.client + return serializers.ModelSerializer.create(self, validated_data) + +class PriceSerializer(serializers.ModelSerializer): + get_employee_id = serializers.IntegerField(write_only=True) + get_work_type_id = serializers.IntegerField(write_only=True) + work_type = WorkTypeSerializer(read_only=True) + + class Meta: + model = Price + fields = ('id', 'get_employee_id', 'get_work_type_id', 'work_type', 'amount', 'deleted') + read_only_fields =('deleted',) + depth = 1 + + def create(self, validated_data): + user = self.context['request'].user + + if not Client.objects.filter(user=user).exists(): + raise serializers.ValidationError("User is not a client.") + + client = user.client + manage_objs = Manage.objects.filter(client=client).filter(deleted=False) + employee_id = validated_data.pop('get_employee_id') + employee = get_object_or_404(manage_objs, id=employee_id) + worktype_objs = WorkType.objects.filter(client=client).filter(deleted=False) + work_type_id = validated_data.pop('get_work_type_id') + work_type = get_object_or_404(worktype_objs, id=work_type_id) + + if not employee.approved: + raise serializers.ValidationError("Employee hasn't approved employment yet.") + + if Price.objects.filter(management=employee) \ + .filter(work_type=work_type) \ + .filter(deleted=False).exists(): + raise serializers.ValidationError("Price for this work already defined.") + + validated_data['management'] = employee + validated_data['work_type'] = work_type + return serializers.ModelSerializer.create(self, validated_data) + +class EmployeeUserSerializer(serializers.ModelSerializer): + userinfo = UserInfoSerializer(read_only=True) + + class Meta: + model = User + fields = ('is_active', 'first_name', 'last_name', 'email', 'userinfo') + depth = 1 + +class EmployeeSerializer(serializers.ModelSerializer): + provider_email = serializers.EmailField(write_only=True) + provider = serializers.SerializerMethodField() + prices = PriceSerializer(many=True, read_only=True) + + class Meta: + model = Manage + fields = ('id', 'provider', 'provider_email', 'note', 'approved', 'prices', 'deleted') + read_only_fields = ('approved', 'deleted') + + def get_provider(self, obj): + if obj.approved == True: + user = User.objects.get(provider=obj.provider) + return EmployeeUserSerializer(user).data + else: + return obj.provider.user.email + + def create(self, validated_data): + user = self.context['request'].user + + if not Client.objects.filter(user=user).exists(): + raise serializers.ValidationError("User is not a client.") + + client = user.client + provider_email = validated_data.pop('provider_email') + provider = get_object_or_404(Provider, user__email=provider_email) + + if Manage.objects.filter(client=client) \ + .filter(provider=provider) \ + .filter(deleted=False).exists(): + raise serializers.ValidationError("Employee already added.") + + validated_data['client'] = client + validated_data['provider'] = provider + return serializers.ModelSerializer.create(self, validated_data) class ClientSerializer(serializers.ModelSerializer): + work_types = WorkTypeSerializer(many=True, read_only=True) + employees = EmployeeSerializer(many=True, read_only=True) + class Meta: model = Client - fields = ('user', 'business_number') + fields = ('business_number', 'work_types', 'employees') + + def create(self, validated_data): + user = self.context['request'].user + + if Provider.objects.filter(user=user).exists(): + raise serializers.ValidationError("User is already a Provider.") + + validated_data['user'] = user + return serializers.ModelSerializer.create(self, validated_data) + +class EmployerUserSerializer(serializers.ModelSerializer): + userinfo = UserInfoSerializer(read_only=True) + + class Meta: + model = User + fields = ('is_active', 'first_name', 'last_name', 'email', 'userinfo') + depth = 1 + +class EmployerSerializer(serializers.ModelSerializer): + client = serializers.SerializerMethodField() + prices = PriceSerializer(many=True, read_only=True) + + class Meta: + model = Manage + fields = ('id', 'client', 'note', 'approved', 'prices') + read_only_fields = ('note',) + + def get_client(self, obj): + if obj.approved == True: + user = User.objects.get(client=obj.client) + return EmployerUserSerializer(user).data + else: + return obj.client.user.first_name + ' ' + obj.client.user.last_name class ProviderSerializer(serializers.ModelSerializer): + employers = EmployerSerializer(many=True, read_only=True) + class Meta: model = Provider - fields = ('user', 'sin') + fields = ('sin', 'employers') + + def create(self, validated_data): + user = self.context['request'].user + + if Client.objects.filter(user=user).exists(): + raise serializers.ValidationError("User is already a Client.") + + validated_data['user'] = user + return serializers.ModelSerializer.create(self, validated_data) class UserSerializer(serializers.ModelSerializer): - userinfo = UserInfoSerializer(read_only=True, allow_null=True) - client = ClientSerializer(allow_null=True) - provider = ProviderSerializer(allow_null=True) + userinfo = UserInfoSerializer(read_only=True) + client = ClientSerializer(read_only=True) + provider = ProviderSerializer(read_only=True) class Meta: model = User fields = ('username', 'is_active', 'first_name', 'last_name', 'email', 'userinfo', 'client', 'provider') depth = 1 + +class CShiftSerializer(serializers.ModelSerializer): + get_price_id = serializers.IntegerField(write_only=True) + + class Meta: + model = Shift + fields = ('id', 'chart', 'get_price_id', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted') + read_only_fields = ('chart', 'price', 'set_price', 'set_date', 'actual_start', 'actual_end', 'amount', 'deleted') + + def create(self, validated_data): + user = self.context['request'].user + + if not Client.objects.filter(user=user).exists(): + raise serializers.ValidationError("User is not a client.") + + client = user.client + price_objs = Price.objects.filter(management__client=client).filter(deleted=False) + price_id = validated_data.pop('get_price_id') + price = get_object_or_404(price_objs, id=price_id) + + if price.management.deleted: + raise serializers.ValidationError("Employment relationship was deleted.") + if price.work_type.deleted: + raise serializers.ValidationError("Type of work was deleted.") + if not price.management.approved: + raise serializers.ValidationError("Employee hasn't approved employment yet.") + + now = datetime.now(timezone.utc) + set_start = validated_data['set_start'] + set_end = validated_data['set_end'] + + if set_start > set_end: + raise serializers.ValidationError("Shift ends before it starts.") + if set_start > set_end - timedelta(hours=1): + raise serializers.ValidationError("Shift is less than one hour long.") + if set_start < now or set_end < now: + raise serializers.ValidationError("Shift is in the past.") + + # TODO: Make sure provider isn't double-booked????? + + validated_data['set_date'] = set_start.date() + validated_data['price'] = price + return serializers.ModelSerializer.create(self, validated_data) + +class PShiftSerializer(serializers.ModelSerializer): + action = serializers.ChoiceField(write_only=True, choices=['checkin', 'checkout']) + + class Meta: + model = Shift + fields = ('id', 'action', 'chart', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted') + read_only_fields = ('id', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted') + + def update(self, instance, validated_data): + action = validated_data['action'] + chart = validated_data['chart'] + + if action == 'checkin': + if instance.deleted: + raise serializers.ValidationError("Shift was deleted.") + if instance.actual_start: + raise serializers.ValidationError("Already checked in.") + if chart: + raise serializers.ValidationError("Can only provide chart on checkout.") + + instance.actual_start = datetime.now(timezone.utc) + instance.set_price = instance.price.amount + + elif action == 'checkout': + if not instance.actual_start: + raise serializers.ValidationError("Need to check in first.") + if instance.actual_end: + raise serializers.ValidationError("Already checked out.") + if not chart: + raise serializers.ValidationError("No chart info provided.") + + instance.actual_end = datetime.now(timezone.utc) + shift_length = instance.actual_end - instance.actual_start + instance.amount = Decimal(shift_length.total_seconds()) / 3600 * instance.set_price + instance.chart = chart + + instance.save() + return instance diff --git a/caremyway/api/views.py b/caremyway/api/views.py index 6f6626b..e38cd35 100644 --- a/caremyway/api/views.py +++ b/caremyway/api/views.py @@ -1,12 +1,11 @@ from django.shortcuts import render - from django.contrib.auth.models import User, Group -from caremyway.api.models import UserInfo, Client, Provider +from caremyway.api.models import UserInfo, Client, Provider, WorkType, Manage, Price, Shift from rest_framework import viewsets, permissions, status from rest_framework.decorators import api_view from rest_framework.response import Response -from caremyway.api.serializers import UserSerializer, UserInfoSerializer, ClientSerializer, ProviderSerializer -from caremyway.api.permissions import UserTypePermission +from rest_framework.views import APIView +from caremyway.api.serializers import UserSerializer, UserInfoSerializer, ClientSerializer, ProviderSerializer, WorkTypeSerializer, EmployeeSerializer, EmployerSerializer, PriceSerializer, CShiftSerializer, PShiftSerializer class UserViewSet(viewsets.ModelViewSet): lookup_field = 'username' @@ -16,52 +15,111 @@ class UserViewSet(viewsets.ModelViewSet): http_method_names = ['get', 'head', 'put', 'options'] def get_queryset(self): - user = self.request.user - if user.is_staff: - return User.objects.all().order_by('-date_joined') - else: - return User.objects.filter(username=user) + return User.objects.filter(username=self.request.user) class UserInfoViewSet(viewsets.ModelViewSet): lookup_field = "user__username" serializer_class = UserInfoSerializer + # Disallow DELETE http_method_names = ['get', 'post', 'head', 'put', 'options'] def get_queryset(self): - user = self.request.user - if user.is_staff: - return UserInfo.objects.all().order_by('-user__date_joined') - else: - return UserInfo.objects.filter(user__username=user) + return UserInfo.objects.filter(user__username=self.request.user) class ClientViewSet(viewsets.ModelViewSet): lookup_field = "user__username" serializer_class = ClientSerializer + # Disallow DELETE http_method_names = ['get', 'post', 'head', 'put', 'options'] - permission_classes = (UserTypePermission,) def get_queryset(self): - user = self.request.user - if user.is_staff: - return Client.objects.all().order_by('-user__date_joined') - else: - return Client.objects.filter(user__username=user) + return Client.objects.filter(user__username=self.request.user) class ProviderViewSet(viewsets.ModelViewSet): lookup_field = "user__username" serializer_class = ProviderSerializer + # Disallow DELETE http_method_names = ['get', 'post', 'head', 'put', 'options'] - permission_classes = (UserTypePermission,) def get_queryset(self): - user = self.request.user - if user.is_staff: - return Provider.objects.all().order_by('-user__date_joined') - else: - return Provider.objects.filter(user__username=user) + return Provider.objects.filter(user__username=self.request.user) + +class WorkTypeViewSet(viewsets.ModelViewSet): + serializer_class = WorkTypeSerializer + + def get_queryset(self): + return WorkType.objects.filter(client__user__username=self.request.user) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.deleted = True + instance.save() + return Response(status=status.HTTP_204_NO_CONTENT) + +class EmployeeViewSet(viewsets.ModelViewSet): + serializer_class = EmployeeSerializer + + def get_queryset(self): + return Manage.objects.filter(client__user__username=self.request.user) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.deleted = True + instance.save() + return Response(status=status.HTTP_204_NO_CONTENT) + +class EmployerViewSet(viewsets.ModelViewSet): + serializer_class = EmployerSerializer + + # Disallow creation and deletions of relationships + http_method_names = ['get', 'head', 'put', 'options'] + + def get_queryset(self): + return Manage.objects.filter(provider__user__username=self.request.user) + +class PriceViewSet(viewsets.ModelViewSet): + serializer_class = PriceSerializer + + def get_queryset(self): + return Price.objects.filter(management__client__user__username=self.request.user) + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + instance.deleted = True + instance.save() + return Response(status=status.HTTP_204_NO_CONTENT) + +class CShiftViewSet(viewsets.ModelViewSet): + serializer_class = CShiftSerializer + + def get_queryset(self): + return Shift.objects.filter(price__management__client__user__username=self.request.user) \ + .filter(deleted=False) \ + .order_by('-set_start') + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + + if instance.actual_start: + return Response("Shift already started.", status=status.HTTP_400_BAD_REQUEST) + + instance.deleted = True + instance.save() + return Response(status=status.HTTP_204_NO_CONTENT) + +class PShiftViewSet(viewsets.ModelViewSet): + serializer_class = PShiftSerializer + + # Disallow creation and deletions of relationships + http_method_names = ['get', 'head', 'put', 'options'] + + def get_queryset(self): + return Shift.objects.filter(price__management__provider__user__username=self.request.user) \ + .filter(deleted=False) \ + .order_by('-set_start') @api_view() def null_view(request): diff --git a/caremyway/urls.py b/caremyway/urls.py index ecdc707..388a18a 100644 --- a/caremyway/urls.py +++ b/caremyway/urls.py @@ -14,6 +14,7 @@ Including another URLconf 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include +from django.contrib import admin from rest_framework import routers from caremyway.api import views @@ -22,11 +23,18 @@ router.register(r'user', views.UserViewSet, 'user') router.register(r'userinfo', views.UserInfoViewSet, 'userinfo') router.register(r'client', views.ClientViewSet, 'client') router.register(r'provider', views.ProviderViewSet, 'provider') +router.register(r'worktype', views.WorkTypeViewSet, 'worktype') +router.register(r'employee', views.EmployeeViewSet, 'employee') +router.register(r'employer', views.EmployerViewSet, 'employer') +router.register(r'price', views.PriceViewSet, 'price') +router.register(r'cshift', views.CShiftViewSet, 'cshift') +router.register(r'pshift', views.PShiftViewSet, 'pshift') # Wire up our API using automatic URL routing. # Additionally, we include login URLs for the browsable API. urlpatterns = [ url(r'^', include(router.urls)), + url(r'^admin/', include(admin.site.urls)), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^rest-auth/registration/account-email-verification-sent/', views.null_view, name='account_email_verification_sent'), url(r'^rest-auth/registration/account-confirm-email/', views.null_view, name='account_confirm_email'),