diff --git a/.gitignore b/.gitignore index f242594..b83da07 100755 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ webpack-stats-prod.json # Cache .cache/ +.pytest_cache/ # OSX .DS_Store diff --git a/Pipfile b/Pipfile index 853e9fc..9d56ab7 100644 --- a/Pipfile +++ b/Pipfile @@ -25,6 +25,7 @@ django-taggit = "*" python-dateutil = "*" "psycopg2" = "*" "psycopg2-binary" = "*" +djangorestframework = "*" [dev-packages] coverage = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 9665001..09e1d9d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8be0d2bb76c1723a1cc2bb21044f188642616e0255d69a4a89256c6ec357c244" + "sha256": "167e74df13423665b6cec80c84d2156978fa696ddc27e7a7d24c075ffb33d245" }, "pipfile-spec": 6, "requires": {}, @@ -37,18 +37,18 @@ }, "django": { "hashes": [ - "sha256:2d8b9eed8815f172a8e898678ae4289a5e9176bc08295676eff4228dd638ea61", - "sha256:d81a1652963c81488e709729a80b510394050e312f386037f26b54912a3a10d0" + "sha256:26b34f4417aa38d895b6b5307177b51bc3f4d53179d8696a5c19dcb50582523c", + "sha256:71d1a584bb4ad2b4f933d07d02c716755c1394feaac1ce61ce37843ac5401092" ], "index": "pypi", - "version": "==2.0.4" + "version": "==2.0.5" }, "django-allauth": { "hashes": [ - "sha256:7b31526cccd1c46f9f09acf0703068e8a9669337d29eb065f7e8143c2d897339" + "sha256:7d9646e3560279d6294ebb4c361fef829708d106da697658cf158bf2ca57b474" ], "index": "pypi", - "version": "==0.35.0" + "version": "==0.36.0" }, "django-anymail": { "hashes": [ @@ -69,10 +69,10 @@ }, "django-bootstrap3": { "hashes": [ - "sha256:9f56fa30a2c32142a2785148816cc4e14e7efc8699c5f87a8cb5be162a81658d" + "sha256:1a5cc3583a208f423b8a081d3fb432957a3e82416512cbafc4a3b10f27cc884a" ], "index": "pypi", - "version": "==9.1.0" + "version": "==10.0.1" }, "django-braces": { "hashes": [ @@ -122,6 +122,14 @@ "index": "pypi", "version": "==0.6.0" }, + "djangorestframework": { + "hashes": [ + "sha256:b6714c3e4b0f8d524f193c91ecf5f5450092c2145439ac2769711f7eba89a9d9", + "sha256:c375e4f95a3a64fccac412e36fb42ba36881e52313ec021ef410b40f67cddca4" + ], + "index": "pypi", + "version": "==3.8.2" + }, "gevent": { "hashes": [ "sha256:0901975628790e8a57fc92bb7062e5b856edea48c8de9caf36cfda14eae07329", @@ -181,11 +189,11 @@ }, "gunicorn": { "hashes": [ - "sha256:eb8d8924b117a609fae9f8cd85df0cad3535dd613fdbcdbba3ee88d5459f1d4f", - "sha256:f5ca088d029fe3cea166c59bb43b7ccc9c850fe25af3da61350fe712c5cc5aa2" + "sha256:7ef2b828b335ed58e3b64ffa84caceb0a7dd7c5ca12f217241350dec36a1d5dc", + "sha256:bc59005979efb6d2dd7d5ba72d99f8a8422862ad17ff3a16e900684630dd2a10" ], "index": "pypi", - "version": "==19.8.0" + "version": "==19.8.1" }, "idna": { "hashes": [ @@ -463,10 +471,10 @@ }, "attrs": { "hashes": [ - "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", - "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" + "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", + "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" ], - "version": "==17.4.0" + "version": "==18.1.0" }, "backcall": { "hashes": [ @@ -582,11 +590,11 @@ }, "django": { "hashes": [ - "sha256:2d8b9eed8815f172a8e898678ae4289a5e9176bc08295676eff4228dd638ea61", - "sha256:d81a1652963c81488e709729a80b510394050e312f386037f26b54912a3a10d0" + "sha256:26b34f4417aa38d895b6b5307177b51bc3f4d53179d8696a5c19dcb50582523c", + "sha256:71d1a584bb4ad2b4f933d07d02c716755c1394feaac1ce61ce37843ac5401092" ], "index": "pypi", - "version": "==2.0.4" + "version": "==2.0.5" }, "django-coverage-plugin": { "hashes": [ @@ -643,11 +651,11 @@ }, "factory-boy": { "hashes": [ - "sha256:bd5a096d0f102d79b6c78cef1c8c0b650f2e1a3ecba351c735c6d2df8dabd29c", - "sha256:be2abc8092294e4097935a29b4e37f5b9ed3e4205e2e32df215c0315b625995e" + "sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca", + "sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f" ], "index": "pypi", - "version": "==2.10.0" + "version": "==2.11.1" }, "faker": { "hashes": [ @@ -665,10 +673,10 @@ }, "flask": { "hashes": [ - "sha256:7fab1062d11dd0038434e790d18c5b9133fd9e6b7257d707c4578ccc1e38b67c", - "sha256:b1883637bbee4dc7bc98d900792d0a304d609fce0f5bd9ca91d1b6457e5918dd" + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" ], - "version": "==1.0" + "version": "==1.0.2" }, "flask-assets": { "hashes": [ @@ -877,10 +885,10 @@ }, "maildump": { "hashes": [ - "sha256:7b994bf28c7179b60f005978214faa1046d9489a983a70711f64f873c5b9db08" + "sha256:1b546fa056046f909eb634ee7c84592b31f39dc00f9df29fbb14b52af915fc43" ], "index": "pypi", - "version": "==0.5.4" + "version": "==0.5.6" }, "markupsafe": { "hashes": [ @@ -977,8 +985,6 @@ }, "pycodestyle": { "hashes": [ - "sha256:1ec08a51c901dfe44921576ed6e4c1f5b7ecbad403f871397feedb5eb8e4fa14", - "sha256:5ff2fbcbab997895ba9ead77e1b38b3ebc2e5c3b8a6194ef918666e4c790a00e", "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9" ], @@ -1057,19 +1063,19 @@ }, "pytest-flake8": { "hashes": [ - "sha256:3996de65a1219697596acac755090c70c47ec901edc438ea88ce1aa6098fb905", - "sha256:43598dfa211242525897866ff35fa553ebd88177383783079099057c7ad04331" + "sha256:e5cdc4f459c9436ac6c649e428a014bb5988605858549397374ec29a776cae68", + "sha256:ec248d4a215d6c7cd9d3ca48f365ece0e3892b46d626c22a95ccc80188ff35ed" ], "index": "pypi", - "version": "==1.0.0" + "version": "==1.0.1" }, "pytest-isort": { "hashes": [ - "sha256:e92798127e21d22513c62070989f0fb3b712650e48a4db13e5b8e8034d367cfe", - "sha256:fc315e3ceac576f2b7000c4716a55063c45ed60c169ba2ede5a6b4aef97ce13e" + "sha256:ce36b62ca1108e16a8b8fc0d5a82302ba0796a3c352b093e13ccbb18e772edc6", + "sha256:d4d195ecfe33d81e258d251b2679b32216bad84131fb41984da22d9d0328a6fe" ], "index": "pypi", - "version": "==0.1.0" + "version": "==0.2.0" }, "pytest-sugar": { "hashes": [ diff --git a/clock/contracts/api/__init__.py b/clock/contracts/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clock/contracts/api/serializers.py b/clock/contracts/api/serializers.py new file mode 100644 index 0000000..b93099c --- /dev/null +++ b/clock/contracts/api/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from clock.contracts.models import Contract + + +class ContractEndDateSerializer(serializers.ModelSerializer): + class Meta: + model = Contract + fields = ('end_date', ) diff --git a/clock/contracts/api/urls.py b/clock/contracts/api/urls.py new file mode 100644 index 0000000..219b283 --- /dev/null +++ b/clock/contracts/api/urls.py @@ -0,0 +1,9 @@ +from django.urls import include, path +from rest_framework import routers + +from clock.contracts.api import views + +router = routers.DefaultRouter() +router.register('end_date', views.ContractEndDateViewSet, base_name='end_date') + +urlpatterns = [path('', include(router.urls))] diff --git a/clock/contracts/api/views.py b/clock/contracts/api/views.py new file mode 100644 index 0000000..8fd6fd4 --- /dev/null +++ b/clock/contracts/api/views.py @@ -0,0 +1,15 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import permissions, viewsets +from rest_framework.response import Response + +from clock.contracts.api.serializers import ContractEndDateSerializer +from clock.contracts.models import Contract + + +class ContractEndDateViewSet(viewsets.ViewSet): + permission_classes = (permissions.IsAuthenticated, ) + + def retrieve(self, request, pk=None): + contract = get_object_or_404(Contract, pk=pk, employee=request.user) + serializer = ContractEndDateSerializer(contract) + return Response(serializer.data) diff --git a/clock/shifts/api/__init__.py b/clock/shifts/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clock/shifts/api/serializers.py b/clock/shifts/api/serializers.py new file mode 100644 index 0000000..6380ee0 --- /dev/null +++ b/clock/shifts/api/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from clock.shifts.models import Shift + + +class ShiftSerializer(serializers.ModelSerializer): + class Meta: + model = Shift + fields = ('started', 'finished', 'employee', 'contract') diff --git a/clock/shifts/api/tests/__init__.py b/clock/shifts/api/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clock/shifts/api/tests/test_views.py b/clock/shifts/api/tests/test_views.py new file mode 100644 index 0000000..02433d5 --- /dev/null +++ b/clock/shifts/api/tests/test_views.py @@ -0,0 +1,102 @@ +from collections import OrderedDict + +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from clock.shifts.factories import UserFactory + + +@pytest.fixture +def client(user): + client = APIClient() + client.force_authenticate(user=user) + return client + + +@pytest.fixture +def user(): + return UserFactory() + + +@pytest.mark.django_db +def test_shift_overlap_api_start_after_finish(client): + """Fail when the finished date is smaller than the started.""" + url = reverse( + 'api:overlap-list', + kwargs={ + 'started': '2018-05-02', + 'finished': '2018-05-01', + 'contract': 1, + 'reoccuring': 'DAILY', + 'pk': 0 + } + ) + + response = client.get(url) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +@pytest.mark.parametrize( + ['reoccurence', 'status_code'], [ + ('ONCE', status.HTTP_400_BAD_REQUEST), ('DAILY', status.HTTP_200_OK), + ('WEEKLY', status.HTTP_200_OK), ('MONTHLY', status.HTTP_200_OK) + ] +) +def test_shift_overlap_api_reoccurence_string( + client, reoccurence, status_code +): + """Check the response depending on the reoccurence string.""" + url = reverse( + 'api:overlap-list', + kwargs={ + 'started': '2018-05-01', + 'finished': '2018-05-02', + 'contract': 1, + 'reoccuring': reoccurence, + 'pk': 0 + } + ) + + response = client.get(url) + assert response.status_code == status_code + + +@pytest.mark.django_db +def test_shift_overlap_api_generate_new_shifts(client): + """Test we can create new shifts, when not overlapping with anything.""" + url = reverse( + 'api:overlap-list', + kwargs={ + 'started': '2018-05-01 05:00', + 'finished': '2018-05-02 08:00', + 'contract': 1, + 'reoccuring': 'DAILY', + 'pk': 0 + } + ) + + response = client.get(url) + assert response.status_code == status.HTTP_200_OK + + shifts = [ + { + 'started': '2018-05-01T05:00:00+02:00', + 'finished': '2018-05-01T08:00:00+02:00', + 'employee': 6, + 'contract': None + }, { + 'started': '2018-05-02T05:00:00+02:00', + 'finished': '2018-05-02T08:00:00+02:00', + 'employee': 6, + 'contract': None + } + ] + shifts = [OrderedDict(shift) for shift in shifts] + + without_overlaps = response.data['without_overlap'] + with_overlaps = response.data['with_overlap'] + assert with_overlaps == [] + assert without_overlaps == shifts diff --git a/clock/shifts/api/urls.py b/clock/shifts/api/urls.py new file mode 100644 index 0000000..54b84b1 --- /dev/null +++ b/clock/shifts/api/urls.py @@ -0,0 +1,20 @@ +# from django.urls import include, path +# from rest_framework import routers +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns + +from clock.shifts.api import views + +# router = routers.DefaultRouter() +# router.register('shifts', views.ShiftOverlapView, base_name='test') + +urlpatterns = [ + path('', views.api_root), + path( + 'overlap//////', + views.ShiftOverlapView.as_view(), + name='overlap-list' + ) +] + +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/clock/shifts/api/views.py b/clock/shifts/api/views.py new file mode 100644 index 0000000..3a5faad --- /dev/null +++ b/clock/shifts/api/views.py @@ -0,0 +1,100 @@ +from datetime import datetime + +from dateutil import parser +from dateutil.rrule import rrule +from django.http import Http404 +from django.utils import timezone +from rest_framework import permissions, status +from rest_framework.decorators import api_view +from rest_framework.response import Response +from rest_framework.reverse import reverse +from rest_framework.views import APIView + +from clock.contracts.models import Contract +from clock.shifts.api.serializers import ShiftSerializer +from clock.shifts.models import Shift +from clock.shifts.utils import ( + FREQUENCIES, + get_shifts_to_check_for_overlaps, + sort_overlapping_shifts, +) + + +@api_view(['GET']) +def api_root(request, format=None): + return Response( + { + 'overlap': + reverse('api:overlap-list', request=request, format=format) + } + ) + + +class ShiftOverlapView(APIView): + permission_classes = (permissions.IsAuthenticated, ) + + def get_object(self, started, finished, contract, reoccuring, pk=None): + shifts = get_shifts_to_check_for_overlaps( + started, + finished, + self.request.user, + contract, + reoccuring=reoccuring, + exclude_shift=pk + ) + + return shifts + + def get(self, request, started, finished, contract, reoccuring, pk): + started = parser.parse(started, fuzzy=True) + finished = parser.parse(finished, fuzzy=True) + + if (started >= finished) or (reoccuring not in FREQUENCIES): + return Response(status=status.HTTP_400_BAD_REQUEST) + + try: + contract = Contract.objects.get(pk=contract) + except Contract.DoesNotExist: + contract = None + + shifts = self.get_object( + started.date(), finished.date(), contract, reoccuring, pk + ) + + if shifts: + good_shifts, bad_shifts = sort_overlapping_shifts( + started, finished, self.request.user, contract, shifts + ) + serialized_data = { + 'without_overlap': ShiftSerializer(good_shifts, + many=True).data, + 'with_overlap': ShiftSerializer(bad_shifts, many=True).data, + } + return Response(serialized_data) + + dates = list( + rrule( + freq=FREQUENCIES[reoccuring], dtstart=started, until=finished + ) + ) + start_time = started.time() + finish_time = finished.time() + + good_shifts = [] + for date in dates: + started = timezone.make_aware(datetime.combine(date, start_time)) + finished = timezone.make_aware(datetime.combine(date, finish_time)) + shift = Shift( + employee=request.user, + contract=contract, + started=started, + finished=finished + ) + good_shifts.append(shift) + + serialized_data = { + 'without_overlap': ShiftSerializer(good_shifts, many=True).data, + 'with_overlap': [] + } + + return Response(serialized_data) diff --git a/clock/shifts/forms.py b/clock/shifts/forms.py index 0d8f2be..ae1293f 100755 --- a/clock/shifts/forms.py +++ b/clock/shifts/forms.py @@ -297,6 +297,8 @@ def __init__(self, *args, **kwargs): self.helper = FormHelper(self) self.helper.form_action = '.' self.helper.form_method = 'post' + self.helper.attrs = {'name': 'shiftForm'} + self.helper.form_id = 'shiftForm' self.helper.layout = Layout( Field( 'started', template='shift/fields/datetimepicker_field.html' @@ -312,7 +314,7 @@ def __init__(self, *args, **kwargs): FormActions( HTML(cancel_html), Submit( - 'submit', + 'submitShiftForm', add_input_text, css_class='btn btn-primary pull-right' ), @@ -341,7 +343,7 @@ def clean(self): reoccuring = self.cleaned_data.get('reoccuring') if reoccuring != 'ONCE' and self.cleaned_data.get( 'contract' - ).end_date and ( + ) and self.cleaned_data.get('contract').end_date and ( self.cleaned_data.get('end_date') > self.cleaned_data.get('contract').end_date ): @@ -366,10 +368,14 @@ def save(self, commit=True): if reoccuring != 'ONCE': # Populate a dictionary with all values that we need to create new # Shifts. + contract = self.cleaned_data.get('contract', None) + if contract is not None: + contract = contract.pk + data = {} for field in ['key', 'note', 'tags', 'end_date']: data[field] = self.cleaned_data.get(field) - data['contract'] = self.cleaned_data.get('contract').pk + data['contract'] = contract data['duration'] = self.instance.duration data['reoccuring'] = 'ONCE' started = self.cleaned_data.get('started') diff --git a/clock/shifts/tests/test_utils.py b/clock/shifts/tests/test_utils.py index f6e7f00..b6d96b1 100644 --- a/clock/shifts/tests/test_utils.py +++ b/clock/shifts/tests/test_utils.py @@ -1,10 +1,163 @@ """Tests for the shift utilities.""" +import pytest +import pytz +from django.contrib.auth import get_user_model +from django.utils import timezone from test_plus import TestCase from clock.contracts.models import Contract from clock.shifts.factories import ShiftFactory, UserFactory +from clock.shifts.forms import ShiftForm from clock.shifts.models import Shift -from clock.shifts.utils import get_current_shift, get_last_shifts +from clock.shifts.utils import ( + get_current_shift, + get_last_shifts, + get_shifts_to_check_for_overlaps, + sort_overlapping_shifts, +) + + +@pytest.fixture +@pytest.mark.django_db +def setup_shifts(): + user = UserFactory() + contract = Contract.objects.create( + employee=user, department='Test department', hours='50' + ) + + started = timezone.datetime(2018, 4, 1, 8, 30) + finished = timezone.datetime(2018, 4, 1, 17, 0) + end_date = timezone.datetime(2018, 6, 30) + form = ShiftForm( + data={ + 'started': started, + 'finished': finished, + 'reoccuring': 'DAILY', + 'end_date': end_date, + 'employee': user, + 'contract': contract.pk + }, + **{ + 'user': user, + 'view': None + } + ) + form.save() + + +@pytest.fixture +def get_user(): + user = get_user_model() + return user.objects.all()[0] + + +@pytest.fixture +def get_contract(): + return Contract.objects.all()[0] + + +@pytest.mark.django_db +def test_sort_overlapping_shifts(setup_shifts, get_user, get_contract): + """Test that we sort the shifts into the overlapping ones.""" + started = timezone.make_aware( + timezone.datetime(2018, 4, 1, 7), timezone=pytz.utc + ) + finished = timezone.make_aware( + timezone.datetime(2018, 4, 1, 10), timezone=pytz.utc + ) + shifts = Shift.objects.filter( + employee=get_user, + contract=get_contract, + started__lte=finished, + finished__gte=started + ).order_by('-created_at') + + good_shifts, bad_shifts = sort_overlapping_shifts( + started, finished, get_user, get_contract, shifts + ) + assert len(bad_shifts) == 1 + assert not good_shifts + + +@pytest.mark.django_db +def test_sort_overlapping_shifts_without_overlaps( + setup_shifts, get_user, get_contract +): + """Test that we correctly sort shifts into the non-overlapping ones.""" + started = timezone.make_aware(timezone.datetime(2018, 4, 1, 2)) + finished = timezone.make_aware(timezone.datetime(2018, 4, 1, 4)) + shifts = Shift.objects.filter( + employee=get_user, + contract=get_contract, + started__lte=timezone.make_aware(timezone.datetime(2018, 4, 30)), + finished__gte=timezone.make_aware(timezone.datetime(2018, 4, 1)) + ).order_by('-created_at') + + good_shifts, bad_shifts = sort_overlapping_shifts( + started, finished, get_user, get_contract, shifts + ) + assert not bad_shifts + assert len(good_shifts) == 29 + + for shift in good_shifts: + assert shift.started.time() == started.astimezone(pytz.utc).time() + assert shift.finished.time() == finished.astimezone(pytz.utc).time() + + +@pytest.mark.django_db +@pytest.mark.parametrize( + 'start_date, end_date, count', [ + ( + timezone.datetime(2018, 4, 5, 6, 0), + timezone.datetime(2018, 4, 5, 7, 55), 1 + ), (timezone.datetime(2018, 4, 5), timezone.datetime(2018, 4, 6), 2), + (timezone.datetime(2018, 4, 5), timezone.datetime(2018, 4, 9), 5) + ] +) +def test_retrieval_of_shifts_to_check_for_overlaps( + setup_shifts, get_user, get_contract, start_date, end_date, count +): + """Test that we retrieve the correct shifts to check for overlaps.""" + started = timezone.make_aware(start_date) + finished = timezone.make_aware(end_date) + reoccuring = 'DAILY' + shifts = get_shifts_to_check_for_overlaps( + started, finished, get_user, get_contract.pk, reoccuring + ) + assert shifts.count() == count + + +@pytest.mark.django_db +def test_overlap_excludes_shift(setup_shifts, get_user, get_contract): + """Test that we can exclude a shift from the overlap check.""" + started = timezone.datetime(2018, 4, 2, 6, 0) + finished = timezone.datetime(2018, 4, 2, 8, 0) + form = ShiftForm( + data={ + 'started': started, + 'finished': finished, + 'reoccuring': 'ONCE', + 'employee': get_user, + 'contract': get_contract.pk + }, + **{ + 'user': get_user, + 'view': None + } + ) + form.save() + start_date = timezone.make_aware(timezone.datetime(2018, 4, 2)) + end_date = timezone.make_aware(timezone.datetime(2018, 4, 3)) + reoccuring = 'DAILY' + shifts = get_shifts_to_check_for_overlaps( + start_date, + end_date, + get_user, + get_contract.pk, + reoccuring, + exclude_shift=form.instance.pk + ) + assert shifts.count() == 2 class TestUtils(TestCase): diff --git a/clock/shifts/urls.py b/clock/shifts/urls.py index a5e9ce8..3516db9 100755 --- a/clock/shifts/urls.py +++ b/clock/shifts/urls.py @@ -7,16 +7,12 @@ ShiftManualEdit, ShiftMonthContractView, ShiftYearView, - get_contract_end_date, shift_action, ) app_name = 'shift' urlpatterns = [ # Shift URLs - path( - 'ajax/get_contract_id/', get_contract_end_date, name="get_contract_id" - ), # Display the ShiftMonthView as default with the current year-month path('', ShiftMonthContractView.as_view(month_format='%m', ), name="list"), # View to handle all the quick-actions from the dashboard diff --git a/clock/shifts/utils.py b/clock/shifts/utils.py index d0c0519..0dafd65 100755 --- a/clock/shifts/utils.py +++ b/clock/shifts/utils.py @@ -1,10 +1,86 @@ # -*- coding: utf-8 -*- from datetime import datetime +import pytz +from dateutil.rrule import DAILY, MONTHLY, WEEKLY, rrule from django.urls import reverse_lazy +from django.utils import timezone +# from clock.shifts.forms import FREQUENCIES from clock.shifts.models import Shift +FREQUENCIES = {'DAILY': DAILY, 'WEEKLY': WEEKLY, 'MONTHLY': MONTHLY} + + +def get_shifts_to_check_for_overlaps( + start_date, end_date, user, contract, reoccuring, exclude_shift=None +): + # Grab all dates that are between `start_date` and `end_date` + dates = list( + rrule( + freq=FREQUENCIES[reoccuring], dtstart=start_date, until=end_date + ) + ) + first_reoccuring_date = dates[0] + last_reoccuring_date = dates[-1] + + # Convert all dates into datetimes + min_time = datetime.min.time() + max_time = datetime.max.time() + first_reoccuring_date = timezone.make_aware( + datetime.combine(first_reoccuring_date, min_time) + ) + last_reoccuring_date = timezone.make_aware( + datetime.combine(last_reoccuring_date, max_time) + ) + + # Grabs all shifts that are within `first_reoccuring_date` and + # `last_reoccuring_date` + shifts = Shift.objects.filter( + employee=user.pk, + started__lte=last_reoccuring_date, + finished__gte=first_reoccuring_date, + ) + + if contract is not None: + shifts.filter(contract=contract) + + if exclude_shift: + shifts = shifts.exclude(pk=exclude_shift) + + # Filter shifts to those on the day of reoccurence + dates = [datetime.date() for datetime in dates] + shifts = shifts.filter(started__date__in=dates) + + return shifts + + +def sort_overlapping_shifts(started, finished, user, contract, shifts): + """Sort all shifts by non-overlapping and overlapping ones. + + Returns two sorted QuerySets. + """ + good_shifts = [] + bad_shifts = [] + + for shift in shifts: + # Set start and finish datetime for the to-be-created Shift + start = datetime.combine(shift.started.date(), + started.time()).astimezone(pytz.utc) + finish = datetime.combine(shift.started.date(), + finished.time()).astimezone(pytz.utc) + s = Shift( + employee=user, started=start, finished=finish, contract=contract + ) + + if (shift.started <= finish.astimezone(pytz.utc) + ) and (shift.finished >= start.astimezone(pytz.utc)): + bad_shifts.append(s) + else: + good_shifts.append(s) + + return good_shifts, bad_shifts + def get_return_url(request, default_success): """Checks whether the user should be returned to the default_success view or to @@ -47,8 +123,8 @@ def get_return_url(request, default_success): } try: - return_kwargs['contract'] = request.session['last_kwargs'][ - 'contract'] + return_kwargs['contract'] = request.session['last_kwargs' + ]['contract'] except KeyError: return_kwargs['contract'] = '00' @@ -137,7 +213,8 @@ def get_last_shifts(user, count=5): :return: Shift objects or None """ finished_shifts = Shift.objects.filter( - employee=user, finished__isnull=False)[:count] + employee=user, finished__isnull=False + )[:count] if not finished_shifts: return None diff --git a/clock/shifts/views.py b/clock/shifts/views.py index 14bbcb3..ff3e865 100755 --- a/clock/shifts/views.py +++ b/clock/shifts/views.py @@ -3,7 +3,6 @@ from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpResponse, JsonResponse from django.shortcuts import redirect from django.utils import timezone from django.utils.decorators import method_decorator @@ -14,7 +13,6 @@ from django.views.generic.list import ListView from pytz import timezone as p_timezone -from clock.contracts.models import Contract from clock.pages.mixins import UserObjectOwnerMixin from clock.shifts.forms import ClockInForm, ClockOutForm, ShiftForm from clock.shifts.models import Shift @@ -27,21 +25,6 @@ ) -@login_required -def get_contract_end_date(request): - if request.method == 'POST': - contract_id = request.POST.get('contract', 0) - contract = Contract.objects.get(pk=contract_id) - - # Only show data to users that own the requested contract - if contract.employee.pk == request.user.pk: - return JsonResponse({'end_date': contract.end_date}) - - return HttpResponse(status=404) - - return HttpResponse(status=403) - - @require_POST @login_required def shift_action(request): diff --git a/clock/templates/shift/edit.html b/clock/templates/shift/edit.html index 84aedbd..b8e979b 100755 --- a/clock/templates/shift/edit.html +++ b/clock/templates/shift/edit.html @@ -15,6 +15,21 @@ {% endblock breadcrumbs %} {% block container %} +
{% if shift %} @@ -32,6 +47,121 @@

{% trans 'New shift' %}

{% block extra_js %}