From 62d35ed72171571bd8f5ffdac0169d7cab8e7e58 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Thu, 5 Sep 2024 07:46:30 +0200 Subject: [PATCH 01/21] first gen #75 --- .pre-commit-config.yaml | 2 +- shifter/settings.py | 1 + shifts/admin.py | 8 ++- shifts/exchanges.py | 23 ++++++ shifts/migrations/0016_auto_20240904_0814.py | 39 +++++++++++ shifts/models.py | 37 ++++++++++ shifts/permanent_contexts.py | 7 +- shifts/templates/layout/navbar.html | 34 +++++++-- shifts/templates/shift_edit.html | 7 ++ shifts/templates/shifts_market.html | 70 +++++++++++++++++++ shifts/urls/main.py | 4 ++ shifts/views/main.py | 73 ++++++++++++++++++++ 12 files changed, 295 insertions(+), 10 deletions(-) create mode 100644 shifts/migrations/0016_auto_20240904_0814.py create mode 100644 shifts/templates/shifts_market.html diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb93b04..2473dd4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,4 +15,4 @@ repos: rev: 6.0.0 hooks: - id: flake8 - args: ['--max-line-length=160', '--ignore=E203,E402,E722,F403,F405,W503'] + args: ['--max-line-length=160', '--ignore=E203,E402,E722,F403,F405,W503,E231,E702'] diff --git a/shifter/settings.py b/shifter/settings.py index ca4930d..a605e51 100644 --- a/shifter/settings.py +++ b/shifter/settings.py @@ -88,6 +88,7 @@ TEMPLATES = [ "shifts.permanent_contexts.application_context", "shifts.permanent_contexts.rota_maker_role", "shifts.permanent_contexts.nav_bar_context", + "shifts.permanent_contexts.shift_market_count", ], }, }, diff --git a/shifts/admin.py b/shifts/admin.py index ecb3239..98d7fa2 100644 --- a/shifts/admin.py +++ b/shifts/admin.py @@ -4,7 +4,7 @@ from django.contrib import messages # Register your models here. -from shifts.models import ShifterMessage, Revision, Campaign, Slot, Shift, ShiftRole, ShiftID, Contact, ShiftExchangePair, ShiftExchange +from shifts.models import ShifterMessage, Revision, Campaign, Slot, Shift, ShiftRole, ShiftID, Contact, ShiftExchangePair, ShiftExchange, ShiftForMarket @admin.register(Contact) @@ -95,6 +95,12 @@ class ShiftIDAdmin(admin.ModelAdmin): ordering = ("-label",) +@admin.register(ShiftForMarket) +class ShiftForMarketAdmin(admin.ModelAdmin): + model = ShiftForMarket + list_display = ["shift", "offerer", "offered_date", "is_available", "is_taken", "comments"] + + @admin.register(Shift) class ShiftAdmin(admin.ModelAdmin): def MOVE_to_newest_VALID_revision(self, request, queryset): diff --git a/shifts/exchanges.py b/shifts/exchanges.py index 5b511da..55fb945 100644 --- a/shifts/exchanges.py +++ b/shifts/exchanges.py @@ -122,6 +122,29 @@ def is_valid_for_hours_constraints( return len(s1) == 0 & len(s2) == 0, (s1, s2) +def is_valid_for_hours_constraints_test( + shiftExchange: ShiftExchange, + member: Member, + baseRevision: Revision, +) -> tuple: + """ + ПроверÑет, ÑоответÑтвует ли Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð½Ð°Ñ Ñмена правилам времени отдыха (дневного и недельного). + Возвращает кортеж из булева Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¸ ÑпиÑка нарушений. + """ + # Получаем ÑпиÑок вÑех Ñмен поÑле возможного обмена + closeScheduleAfterUpdate = get_exchange_exchange_preview(shiftExchange, baseRevision) + + # ПроверÑем, еÑÑ‚ÑŒ ли Ð½Ð°Ñ€ÑƒÑˆÐµÐ½Ð¸Ñ Ð¿Ð¾ времени отдыха Ð´Ð»Ñ Ð´Ð½ÐµÐ²Ð½Ð¾Ð³Ð¾ и недельного + daily_violations = find_daily_rest_time_violation([x for x in closeScheduleAfterUpdate if x.member == member]) + weekly_violations = find_weekly_rest_time_violation([x for x in closeScheduleAfterUpdate if x.member == member]) + + # ЕÑли еÑÑ‚ÑŒ Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ одно нарушение, то Ñмена не может быть добавлена + is_valid = len(daily_violations) == 0 and len(weekly_violations) == 0 + + # Возвращаем результат проверки и ÑпиÑок нарушений + return is_valid, (daily_violations, weekly_violations) + + def perform_exchange_and_save_backup(shiftExchange: ShiftExchange, approver: Member, revisionBackup: Revision, verbose=False) -> list: """ creates new shifts for the proposed slots, keeps the old ones in the revisionBackup, diff --git a/shifts/migrations/0016_auto_20240904_0814.py b/shifts/migrations/0016_auto_20240904_0814.py new file mode 100644 index 0000000..eca631c --- /dev/null +++ b/shifts/migrations/0016_auto_20240904_0814.py @@ -0,0 +1,39 @@ +# Generated by Django 3.2.16 on 2024-09-04 06:14 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('shifts', '0015_shiftexchange_shiftexchangepair'), + ] + + operations = [ + migrations.AlterField( + model_name='desiderata', + name='type', + field=models.CharField(choices=[('vac', 'Vacation'), ('conf', 'Conference'), ('wfh', 'Work From Home'), ('other', 'Other')], default='vac', max_length=10), + ), + migrations.CreateModel( + name='ShiftForMarket', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('offered_date', models.DateTimeField(default=django.utils.timezone.now)), + ('is_available', models.BooleanField(default=True)), + ('is_taken', models.BooleanField(default=False)), + ('comments', models.TextField(blank=True, null=True)), + ('offerer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('shift', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='shifts.shift')), + ], + options={ + 'verbose_name': 'Shift for market', + 'verbose_name_plural': 'Shifts for market', + 'ordering': ['offered_date'], + }, + ), + ] diff --git a/shifts/models.py b/shifts/models.py index a042c6e..8f07c21 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -305,6 +305,43 @@ class Shift(models.Model): return datetime.datetime.combine(self.date, timeToUse) + datetime.timedelta(days=deltaToAdd) +class ShiftForMarket(models.Model): + shift = models.ForeignKey(Shift, on_delete=DO_NOTHING) + offerer = models.ForeignKey(Member, on_delete=CASCADE) + offered_date = models.DateTimeField(default=timezone.now) + is_available = models.BooleanField(default=True) + is_taken = models.BooleanField(default=False) + comments = models.TextField(blank=True, null=True) + + class Meta: + verbose_name = "Shift for market" + verbose_name_plural = "Shifts for market" + ordering = ["offered_date"] + + def __str__(self): + return f"Shift {self.shift} ordered {self.offerer} on {self.offered_date.strftime(DATE_FORMAT_FULL)}" + + def mark_as_taken(self): + self.is_available = False + self.is_taken = True + self.save() + + def mark_as_available(self): + self.is_available = True + self.is_taken = False + self.save() + + def get_offer_details(self): + return { + "shift": str(self.shift), + "offerer": str(self.offerer), + "offered_date": self.offered_date.strftime(DATE_FORMAT_FULL), + "is_available": self.is_available, + "is_taken": self.is_taken, + "comments": self.comments, + } + + class ShiftExchangePair(models.Model): shift = models.ForeignKey(Shift, on_delete=DO_NOTHING) shift_for_exchange = models.ForeignKey(Shift, on_delete=DO_NOTHING, related_name="for_exchange") diff --git a/shifts/permanent_contexts.py b/shifts/permanent_contexts.py index 2ac0a53..bd04fec 100644 --- a/shifts/permanent_contexts.py +++ b/shifts/permanent_contexts.py @@ -1,5 +1,5 @@ from .activeshift import prepare_active_crew -from .models import Contact +from .models import Contact, ShiftForMarket import django.contrib.messages as messages from members.models import Team @@ -31,6 +31,11 @@ def useful_contact_context(request): return {"useful_contact": contacts} +def shift_market_count(request): + shift_count = ShiftForMarket.objects.filter(is_available=True, shift__member__role=request.user.role).count() + return {"shift_count": shift_count} + + def nav_bar_context(request): teams = Team.objects.all().order_by("name") return {"teams": teams} diff --git a/shifts/templates/layout/navbar.html b/shifts/templates/layout/navbar.html index f401ae0..ad10999 100644 --- a/shifts/templates/layout/navbar.html +++ b/shifts/templates/layout/navbar.html @@ -92,6 +92,26 @@ <li><a class="dropdown-item text-center" href="{% url 'shift-upload' %}">Upload planning</a></li> </ul> </li> + <li class="nav-item position-relative"> + <a class="nav-link {% if request.resolver_match.url_name == 'shifts_market' %}active{% endif %}" href="{% url 'shifts_market' %}"> + <div class="d-flex flex-column align-items-center"> + <i class="fa-solid fa-sign-out-alt fa-1x"></i> + <span class="text-center">Shift Market</span> + {% if shift_count > 0 %} + <span class="badge bg-danger position-absolute top-0 start-100 translate-middle badge rounded-pill">{{ shift_count }}</span> + <!-- bg-primary - Blue --> + <!-- bg-secondary - Gray --> + <!-- bg-success - Green --> + <!-- bg-danger - Red --> + <!-- bg-warning - Yellow --> + <!-- bg-info - Light Blue --> + <!-- bg-light - Light Gray --> + <!-- bg-dark - Black --> + <!-- bg-white - White --> + {% endif %} + </div> + </a> + </li> {% endif %} </ul> @@ -130,11 +150,11 @@ </nav> <style> -.nav-link { - color: #000000; /* Icons color */ - transition: color 0.3s, background-color 0.3s, transform 0.3s; - display: flex; - align-items: center; - padding: 0.5rem 1rem; /* Adjust padding as needed */ -} + .nav-link { + color: #000000; /* Icons color */ + transition: color 0.3s, background-color 0.3s, transform 0.3s; + display: flex; + align-items: center; + padding: 0.5rem 1rem; /* Adjust padding as needed */ + } </style> diff --git a/shifts/templates/shift_edit.html b/shifts/templates/shift_edit.html index 25263b0..911cd8a 100644 --- a/shifts/templates/shift_edit.html +++ b/shifts/templates/shift_edit.html @@ -67,6 +67,13 @@ <button class="btn btn-primary">Update shift details</button> </div> </form> + <form action="{% url 'shifter:add_shift_to_market' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> + {% csrf_token %} + <input type="hidden" name="action_type" value="add_to_market"> + <div class="form-floating"> + <button class="btn btn-primary">Add shift to market</button> + </div> + </form> </div> </div> {%else%} diff --git a/shifts/templates/shifts_market.html b/shifts/templates/shifts_market.html new file mode 100644 index 0000000..5f535a0 --- /dev/null +++ b/shifts/templates/shifts_market.html @@ -0,0 +1,70 @@ +{% extends 'template_main.html' %} +{% load static %} +{% load crispy_forms_tags %} + +{% block body %} + <div class="container"> + <h2>Available Shifts on Market for {{request.user.role}}</h2> + <div class="list-group"> + {% for shift in available_shifts %} + <div class="list-group-item"> + <h5 class="mb-1">{{ shift.shift }}</h5> + <p class="mb-1"><strong>Offered by:</strong> {{ shift.offerer }}</p> + <small><strong>Offered on:</strong> {{ shift.offered_date }}</small> + <p>{{ shift.comments }}</p> + + <div class="btn-group" role="group" aria-label="Shift actions"> + <a href="#" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#confirmTakeModal-{{ shift.id }}"> + Take + </a> + + {% if request.user == shift.offerer %} + <a href="#" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirmDeleteModal-{{ shift.id }}"> + Remove + </a> + {% endif %} + </div> + </div> + + <div class="modal fade" id="confirmTakeModal-{{ shift.id }}" tabindex="-1" aria-labelledby="confirmTakeLabel-{{ shift.id }}" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="confirmTakeLabel-{{ shift.id }}">Confirm Action</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + Are you sure you want to take this shift? + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <a href="{% url 'take_shift' shift.id %}" class="btn btn-success">Confirm</a> + </div> + </div> + </div> + </div> + + <div class="modal fade" id="confirmDeleteModal-{{ shift.id }}" tabindex="-1" aria-labelledby="confirmDeleteLabel-{{ shift.id }}" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="confirmDeleteLabel-{{ shift.id }}">Confirm Delete</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + Are you sure you want to delete this shift? This action cannot be undone. + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <a href="{% url 'delete_shift' shift.id %}" class="btn btn-danger">Remove</a> + </div> + </div> + </div> + </div> + + {% empty %} + <p>No shifts available on the market at the moment.</p> + {% endfor %} + </div> + </div> +{% endblock %} \ No newline at end of file diff --git a/shifts/urls/main.py b/shifts/urls/main.py index 088968d..beecf86 100644 --- a/shifts/urls/main.py +++ b/shifts/urls/main.py @@ -24,6 +24,10 @@ urlpatterns = [ path("shifts", views.shifts, name="shifts"), path("shift/<int:sid>", views.shift_edit, name="shift-edit"), path("shift/<int:sid>/edit", views.shift_edit_post, name="shift-edit-post"), + path("shifts_market/", views.shifts_market, name="shifts_market"), + path("planning/shift_market/take/<int:shift_id>/", views.take_shift, name="take_shift"), + path("shift_market/delete/<int:shift_id>/", views.delete_shift, name="delete_shift"), + path("shift/<int:shift_id>/add_shift_to_market/", views.add_shift_to_market, name="add_shift_to_market"), path("shift/<int:sid>/exchange", views.shift_single_exchange_post, name="shift-single-exchange-post"), path("shift-exchange", views.shiftExchangeRequestCreateOrUpdate, name="shift-exchange-request"), path("shift-exchange/<int:ex_id>", views.shiftExchangeRequestCreateOrUpdate, name="shift-exchange-request"), diff --git a/shifts/views/main.py b/shifts/views/main.py index 3051da1..f4dd864 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -637,6 +637,79 @@ def shift_edit_post(request, sid=None): return HttpResponseRedirect(reverse("shifter:index")) +@require_http_methods(["POST"]) +@csrf_protect +@login_required +def add_shift_to_market(request, shift_id): + shift = get_object_or_404(Shift, id=shift_id) + + if shift.date < timezone.now().date(): + messages.error(request, "You cannot add a shift to the market with a date in the past.") + return redirect("shifter:user") + + if ShiftForMarket.objects.filter(shift=shift, is_available=True).exists(): + messages.error(request, "This shift is already on the market.") + return redirect("shifter:user") + + messages.success(request, "Shift successfully added to the market.") + return redirect("shifter:shifts_market") + + +@login_required +def shifts_market(request): + user_role = request.user.role + + available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("offered_date") + + shift_count = available_shifts.count() + + user_shifts = Shift.objects.filter(member__role=request.user.role, date__gt=timezone.now().date()).order_by("date") + + context = { + "available_shifts": available_shifts, + "user_shifts": user_shifts, + "shift_count": shift_count, + } + return render(request, "shifts_market.html", context) + + +@login_required +def take_shift(request, shift_id): + try: + shift_market = ShiftForMarket.objects.get(id=shift_id, is_available=True) + except ShiftForMarket.DoesNotExist: + messages.error(request, "The shift you tried to take is no longer available.") + return redirect("shifts_market") + + sExDone = perform_simplified_exchange_and_save_backup( + shift_market.shift, request.user, approver=request.user, revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first() + ) + messages.success(request, "Requested shift exchange (update) is now successfully implemented.") + notificationService.notify(sExDone) + + messages.success(request, "You have successfully taken the shift.") + shift_market.mark_as_taken() + return redirect("shifts_market") + + +@login_required +def delete_shift(request, shift_id): + shift_market = get_object_or_404(ShiftForMarket, id=shift_id) + + if request.user != shift_market.offerer: + messages.error(request, "You cannot delete this shift because you did not add it.") + return redirect("shifts_market") + + shift_market.delete() + messages.success(request, "Shift has been deleted from the market.") + return redirect("shifts_market") + + +@login_required +def user_page(request): + return redirect("user") + + @require_http_methods(["POST"]) @csrf_protect @login_required -- GitLab From f430a84dbb5912d2700ec532742883a3e6223984 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Thu, 5 Sep 2024 07:50:22 +0200 Subject: [PATCH 02/21] some fix #75 --- shifts/exchanges.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/shifts/exchanges.py b/shifts/exchanges.py index 55fb945..5b511da 100644 --- a/shifts/exchanges.py +++ b/shifts/exchanges.py @@ -122,29 +122,6 @@ def is_valid_for_hours_constraints( return len(s1) == 0 & len(s2) == 0, (s1, s2) -def is_valid_for_hours_constraints_test( - shiftExchange: ShiftExchange, - member: Member, - baseRevision: Revision, -) -> tuple: - """ - ПроверÑет, ÑоответÑтвует ли Ð¿Ñ€ÐµÐ´Ð»Ð¾Ð¶ÐµÐ½Ð½Ð°Ñ Ñмена правилам времени отдыха (дневного и недельного). - Возвращает кортеж из булева Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð¸ ÑпиÑка нарушений. - """ - # Получаем ÑпиÑок вÑех Ñмен поÑле возможного обмена - closeScheduleAfterUpdate = get_exchange_exchange_preview(shiftExchange, baseRevision) - - # ПроверÑем, еÑÑ‚ÑŒ ли Ð½Ð°Ñ€ÑƒÑˆÐµÐ½Ð¸Ñ Ð¿Ð¾ времени отдыха Ð´Ð»Ñ Ð´Ð½ÐµÐ²Ð½Ð¾Ð³Ð¾ и недельного - daily_violations = find_daily_rest_time_violation([x for x in closeScheduleAfterUpdate if x.member == member]) - weekly_violations = find_weekly_rest_time_violation([x for x in closeScheduleAfterUpdate if x.member == member]) - - # ЕÑли еÑÑ‚ÑŒ Ñ…Ð¾Ñ‚Ñ Ð±Ñ‹ одно нарушение, то Ñмена не может быть добавлена - is_valid = len(daily_violations) == 0 and len(weekly_violations) == 0 - - # Возвращаем результат проверки и ÑпиÑок нарушений - return is_valid, (daily_violations, weekly_violations) - - def perform_exchange_and_save_backup(shiftExchange: ShiftExchange, approver: Member, revisionBackup: Revision, verbose=False) -> list: """ creates new shifts for the proposed slots, keeps the old ones in the revisionBackup, -- GitLab From a5e08c92f18ebbcf45dbf7c11270fa5cf1cd1ad0 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Thu, 5 Sep 2024 08:28:24 +0200 Subject: [PATCH 03/21] fix fix #75 --- shifts/migrations/0016_auto_20240904_0814.py | 33 ++++++++++---------- shifts/templates/shifts_market.html | 2 +- shifts/views/main.py | 12 ++++++- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/shifts/migrations/0016_auto_20240904_0814.py b/shifts/migrations/0016_auto_20240904_0814.py index eca631c..30bcb5b 100644 --- a/shifts/migrations/0016_auto_20240904_0814.py +++ b/shifts/migrations/0016_auto_20240904_0814.py @@ -7,33 +7,34 @@ import django.utils.timezone class Migration(migrations.Migration): - dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('shifts', '0015_shiftexchange_shiftexchangepair'), + ("shifts", "0015_shiftexchange_shiftexchangepair"), ] operations = [ migrations.AlterField( - model_name='desiderata', - name='type', - field=models.CharField(choices=[('vac', 'Vacation'), ('conf', 'Conference'), ('wfh', 'Work From Home'), ('other', 'Other')], default='vac', max_length=10), + model_name="desiderata", + name="type", + field=models.CharField( + choices=[("vac", "Vacation"), ("conf", "Conference"), ("wfh", "Work From Home"), ("other", "Other")], default="vac", max_length=10 + ), ), migrations.CreateModel( - name='ShiftForMarket', + name="ShiftForMarket", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('offered_date', models.DateTimeField(default=django.utils.timezone.now)), - ('is_available', models.BooleanField(default=True)), - ('is_taken', models.BooleanField(default=False)), - ('comments', models.TextField(blank=True, null=True)), - ('offerer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('shift', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='shifts.shift')), + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("offered_date", models.DateTimeField(default=django.utils.timezone.now)), + ("is_available", models.BooleanField(default=True)), + ("is_taken", models.BooleanField(default=False)), + ("comments", models.TextField(blank=True, null=True)), + ("offerer", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("shift", models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to="shifts.shift")), ], options={ - 'verbose_name': 'Shift for market', - 'verbose_name_plural': 'Shifts for market', - 'ordering': ['offered_date'], + "verbose_name": "Shift for market", + "verbose_name_plural": "Shifts for market", + "ordering": ["offered_date"], }, ), ] diff --git a/shifts/templates/shifts_market.html b/shifts/templates/shifts_market.html index 5f535a0..04f46c6 100644 --- a/shifts/templates/shifts_market.html +++ b/shifts/templates/shifts_market.html @@ -67,4 +67,4 @@ {% endfor %} </div> </div> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/shifts/views/main.py b/shifts/views/main.py index f4dd864..7caaa0a 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -642,6 +642,7 @@ def shift_edit_post(request, sid=None): @login_required def add_shift_to_market(request, shift_id): shift = get_object_or_404(Shift, id=shift_id) + member = request.user if shift.date < timezone.now().date(): messages.error(request, "You cannot add a shift to the market with a date in the past.") @@ -651,6 +652,15 @@ def add_shift_to_market(request, shift_id): messages.error(request, "This shift is already on the market.") return redirect("shifter:user") + market_comment = request.POST.get("marketComment", "") + ShiftForMarket.objects.create( + shift=shift, + offerer=member, + offered_date=timezone.now(), + comments=market_comment, + is_available=True, + is_taken=False, + ) messages.success(request, "Shift successfully added to the market.") return redirect("shifter:shifts_market") @@ -663,7 +673,7 @@ def shifts_market(request): shift_count = available_shifts.count() - user_shifts = Shift.objects.filter(member__role=request.user.role, date__gt=timezone.now().date()).order_by("date") + user_shifts = Shift.objects.filter(member=request.user, date__gt=timezone.now().date()).order_by("date") context = { "available_shifts": available_shifts, -- GitLab From 7419565337a237a523bac22a450b90209e9a58f9 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Thu, 5 Sep 2024 13:55:10 +0200 Subject: [PATCH 04/21] fix fix fix --- shifts/migrations/0017_auto_20240905_1227.py | 24 ++++++ .../migrations/0018_shiftformarket_urgency.py | 18 +++++ .../0019_alter_shiftformarket_taken_date.py | 18 +++++ shifts/models.py | 10 +++ shifts/templates/layout/navbar.html | 2 +- shifts/templates/shift_edit.html | 76 ++++++++++++++----- shifts/templates/shifts_market.html | 27 +++++-- shifts/views/main.py | 21 +++-- 8 files changed, 163 insertions(+), 33 deletions(-) create mode 100644 shifts/migrations/0017_auto_20240905_1227.py create mode 100644 shifts/migrations/0018_shiftformarket_urgency.py create mode 100644 shifts/migrations/0019_alter_shiftformarket_taken_date.py diff --git a/shifts/migrations/0017_auto_20240905_1227.py b/shifts/migrations/0017_auto_20240905_1227.py new file mode 100644 index 0000000..2778970 --- /dev/null +++ b/shifts/migrations/0017_auto_20240905_1227.py @@ -0,0 +1,24 @@ +# Generated by Django 3.2.16 on 2024-09-05 10:27 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('shifts', '0016_auto_20240904_0814'), + ] + + operations = [ + migrations.AddField( + model_name='shiftformarket', + name='available_until', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='shiftformarket', + name='taken_date', + field=models.DateTimeField(default=django.utils.timezone.now), + ), + ] diff --git a/shifts/migrations/0018_shiftformarket_urgency.py b/shifts/migrations/0018_shiftformarket_urgency.py new file mode 100644 index 0000000..1b681a7 --- /dev/null +++ b/shifts/migrations/0018_shiftformarket_urgency.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2024-09-05 10:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shifts', '0017_auto_20240905_1227'), + ] + + operations = [ + migrations.AddField( + model_name='shiftformarket', + name='urgency', + field=models.CharField(choices=[('Normal', 'Normal'), ('Urgent', 'Urgent')], default='Normal', max_length=10), + ), + ] diff --git a/shifts/migrations/0019_alter_shiftformarket_taken_date.py b/shifts/migrations/0019_alter_shiftformarket_taken_date.py new file mode 100644 index 0000000..ba92f9a --- /dev/null +++ b/shifts/migrations/0019_alter_shiftformarket_taken_date.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2024-09-05 11:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shifts', '0018_shiftformarket_urgency'), + ] + + operations = [ + migrations.AlterField( + model_name='shiftformarket', + name='taken_date', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/shifts/models.py b/shifts/models.py index 8f07c21..1046565 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -306,9 +306,17 @@ class Shift(models.Model): class ShiftForMarket(models.Model): + URGENCY_CHOICES = [ + ("Normal", "Normal"), + ("Urgent", "Urgent"), + ] + shift = models.ForeignKey(Shift, on_delete=DO_NOTHING) offerer = models.ForeignKey(Member, on_delete=CASCADE) offered_date = models.DateTimeField(default=timezone.now) + available_until = models.DateField(null=True, blank=True) + taken_date = models.DateTimeField(null=True, blank=True) + urgency = models.CharField(max_length=10, choices=URGENCY_CHOICES, default="Normal") is_available = models.BooleanField(default=True) is_taken = models.BooleanField(default=False) comments = models.TextField(blank=True, null=True) @@ -324,11 +332,13 @@ class ShiftForMarket(models.Model): def mark_as_taken(self): self.is_available = False self.is_taken = True + self.taken_date = timezone.now() self.save() def mark_as_available(self): self.is_available = True self.is_taken = False + self.taken_date = None self.save() def get_offer_details(self): diff --git a/shifts/templates/layout/navbar.html b/shifts/templates/layout/navbar.html index ad10999..67e6b81 100644 --- a/shifts/templates/layout/navbar.html +++ b/shifts/templates/layout/navbar.html @@ -95,7 +95,7 @@ <li class="nav-item position-relative"> <a class="nav-link {% if request.resolver_match.url_name == 'shifts_market' %}active{% endif %}" href="{% url 'shifts_market' %}"> <div class="d-flex flex-column align-items-center"> - <i class="fa-solid fa-sign-out-alt fa-1x"></i> + <i class="fa-solid fa-store fa-1x"></i> <span class="text-center">Shift Market</span> {% if shift_count > 0 %} <span class="badge bg-danger position-absolute top-0 start-100 translate-middle badge rounded-pill">{{ shift_count }}</span> diff --git a/shifts/templates/shift_edit.html b/shifts/templates/shift_edit.html index 911cd8a..0ae9173 100644 --- a/shifts/templates/shift_edit.html +++ b/shifts/templates/shift_edit.html @@ -7,7 +7,6 @@ <h2>{{shift}}</h2> <form action="{% url 'shifter:shift-single-exchange-post' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} - <div class="form-select bg-warning"> <label for="shiftMember" class="form-label">Assigned shift member:</label> <p>Note: Updating member will create an approved ShiftExchange with respective notifications!</p> @@ -18,11 +17,11 @@ </select> <br> <div class="form-floating"> - <button class="btn btn-dark">Update shift member</button> + <button class="btn btn-dark">Update shift member</button> </div> </div> - </form> - <form action="{% url 'shifter:shift-edit-post' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> + </form> + <form action="{% url 'shifter:shift-edit-post' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} <hr class="hr" /> <h4>Update current shift</h4> @@ -31,7 +30,7 @@ <select class="form-select" name='shiftRole' id='shiftRole'> <option value="-1">Default: Member Role - {{shift.member.role}}</option> {% for sr in shiftRoles %} - <option value="{{sr.id}}" {%if shift.role == sr%} selected {%endif%}>Shift Role - {{sr.name}}</option> + <option value="{{sr.id}}" {% if shift.role == sr %} selected {% endif %}>Shift Role - {{sr.name}}</option> {% endfor %} </select> </div> @@ -40,46 +39,81 @@ style="height: 100px">{{shift.pre_comment}}</textarea> <label for="preShiftComment">Pre-shift memo</label> </div> - <div class="form-check"> <input class="form-check-input" type="checkbox" name="activeShift" - id="activeShift" {%if shift.is_active%}checked{%endif%} > + id="activeShift" {% if shift.is_active %}checked{% endif %}> <label class="form-check-label" for="activeShift"> Is this shift valid and still in schedule? Note: removing will require admin access to undo! </label> </div> - <div class="form-check"> <input class="form-check-input" type="checkbox" name="cancelledLastMinute" - id="cancelledLastMinute" {%if shift.is_cancelled%}checked{%endif%}> + id="cancelledLastMinute" {% if shift.is_cancelled %}checked{% endif %}> <label class="form-check-label" for="cancelledLastMinute"> Canceled last minute? </label> </div> - <div class="form-floating"> <textarea class="form-control" placeholder="Anything to add?" name="postShiftComment" id="postShiftComment" style="height: 100px">{{shift.post_comment}}</textarea> <label for="postShiftComment">Post shift/cancellation comment</label> </div> - <div class="form-floating"> <button class="btn btn-primary">Update shift details</button> </div> </form> - <form action="{% url 'shifter:add_shift_to_market' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> - {% csrf_token %} - <input type="hidden" name="action_type" value="add_to_market"> - <div class="form-floating"> - <button class="btn btn-primary">Add shift to market</button> + + <div> </div> + + <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addShiftToMarketModal"> + Add shift to market + </button> + + <div class="modal fade" id="addShiftToMarketModal" tabindex="-1" aria-labelledby="addShiftToMarketModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="addShiftToMarketModalLabel">Add Shift to Market</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <form action="{% url 'shifter:add_shift_to_market' shift.id %}" method="POST"> + {% csrf_token %} + <div class="modal-body"> + <div class="form-floating"> + <textarea class="form-control" placeholder="Add a comment" name="marketComment" id="marketComment" style="height: 100px"></textarea> + <label for="marketComment">Comment for the market</label> + </div> + + <div class="form-floating mt-3"> + <input type="date" class="form-control" name="availableUntil" id="availableUntil" required> + <label for="availableUntil">Available Until</label> + </div> + + <div class="form-floating mt-3"> + <select class="form-select" id="urgency" name="urgency" required> + <option value="Normal" selected>Normal</option> + <option value="Urgent">Urgent</option> + </select> + <label for="urgency">Urgency</label> + </div> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> + <button type="submit" class="btn btn-primary">Submit to Market</button> + </div> + </form> + </div> </div> - </form> + </div> </div> </div> -{%else%} -<div class="container mb-5 pb-5"> <h3>You do not have permission to navigate here! Contact OP group for further assistance.</h3></div> -{%endif%} +{% else %} +<div class="container mb-5 pb-5"> + <h3>You do not have permission to navigate here! Contact OP group for further assistance.</h3> +</div> +{% endif %} {% endblock %} -{% block js %} +{% block js %} +<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> {% endblock %} diff --git a/shifts/templates/shifts_market.html b/shifts/templates/shifts_market.html index 04f46c6..386de84 100644 --- a/shifts/templates/shifts_market.html +++ b/shifts/templates/shifts_market.html @@ -3,14 +3,31 @@ {% load crispy_forms_tags %} {% block body %} - <div class="container"> - <h2>Available Shifts on Market for {{request.user.role}}</h2> + <div class="container-fluid mb-5 pb-5"> + <h1 class="text-center mb-3">Available Shifts on Market for {{request.user.role}}</h1> <div class="list-group"> {% for shift in available_shifts %} - <div class="list-group-item"> + <div class="list-group-item + {% if shift.urgency == 'Urgent' %} + border-danger + {% else %} + border-secondary + {% endif %} + "> <h5 class="mb-1">{{ shift.shift }}</h5> <p class="mb-1"><strong>Offered by:</strong> {{ shift.offerer }}</p> <small><strong>Offered on:</strong> {{ shift.offered_date }}</small> + + <p><strong>Urgency:</strong> {{ shift.urgency }}</p> + + <p><strong>Available until:</strong> + {% if shift.available_until %} + {{ shift.available_until }} + {% else %} + Not specified + {% endif %} + </p> + <p>{{ shift.comments }}</p> <div class="btn-group" role="group" aria-label="Shift actions"> @@ -34,7 +51,7 @@ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> - Are you sure you want to take this shift? + Are you sure you want to take this shift? It will be available until {{ shift.available_until }}. </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> @@ -52,7 +69,7 @@ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> - Are you sure you want to delete this shift? This action cannot be undone. + Are you sure you want to delete this shift? This action cannot be undone. The shift is available until {{ shift.available_until }}. </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> diff --git a/shifts/views/main.py b/shifts/views/main.py index 7caaa0a..f5f8c9f 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -653,14 +653,23 @@ def add_shift_to_market(request, shift_id): return redirect("shifter:user") market_comment = request.POST.get("marketComment", "") + available_until = request.POST.get("availableUntil", None) + urgency = request.POST.get("urgency", "Normal") + + if not available_until: + messages.error(request, "Please select a valid 'available until' date.") + return redirect("shifter:user") + + available_until_date = timezone.datetime.strptime(available_until, "%Y-%m-%d").date() + + if available_until_date < timezone.now().date(): + messages.error(request, "The 'available until' date cannot be in the past.") + return redirect("shifter:user") + ShiftForMarket.objects.create( - shift=shift, - offerer=member, - offered_date=timezone.now(), - comments=market_comment, - is_available=True, - is_taken=False, + shift=shift, offerer=member, offered_date=timezone.now(), comments=market_comment, available_until=available_until_date, urgency=urgency ) + messages.success(request, "Shift successfully added to the market.") return redirect("shifter:shifts_market") -- GitLab From 8c12e8f32fd29c0303e8fb30f37d17943528f8eb Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Sat, 7 Sep 2024 11:34:16 +0200 Subject: [PATCH 05/21] WIP few bigger changes along with market - refactor code names (unified function names) - added few urls/views (shift/ shiftForMarket/ shiftExchange) - calendar js fix (to include new data source - new notification on ShiftForMarket - various little ones --- shifter/notifications.py | 32 ++++++- shifts/migrations/0017_auto_20240905_1227.py | 11 ++- .../migrations/0018_shiftformarket_urgency.py | 9 +- .../0019_alter_shiftformarket_taken_date.py | 7 +- shifts/migrations/0020_shiftexchange_type.py | 17 ++++ shifts/models.py | 25 +++++- shifts/permanent_contexts.py | 6 +- shifts/static/js/calendar_script.js | 9 ++ shifts/static/js/calendar_script.min.js | 2 +- shifts/templates/calendar.html | 6 +- shifts/templates/layout/navbar.html | 33 +++---- shifts/templates/shift_edit.html | 23 +++-- shifts/templates/shift_market_email.html | 13 +++ shifts/templates/shiftexchange_edit.html | 19 ++++ shifts/templates/shifts_market.html | 88 +++++++++---------- shifts/templates/user.html | 2 +- shifts/urls/ajax.py | 1 + shifts/urls/main.py | 11 ++- shifts/views/ajax.py | 8 ++ shifts/views/main.py | 50 +++++++++-- 20 files changed, 258 insertions(+), 114 deletions(-) create mode 100644 shifts/migrations/0020_shiftexchange_type.py create mode 100644 shifts/templates/shift_market_email.html create mode 100644 shifts/templates/shiftexchange_edit.html diff --git a/shifter/notifications.py b/shifter/notifications.py index f744a28..dd2d4ca 100644 --- a/shifter/notifications.py +++ b/shifter/notifications.py @@ -5,7 +5,7 @@ Prototype of the shifter-centralised notification implementation and classes. from abc import ABC, abstractmethod from django.template.loader import render_to_string -from shifts.models import Shift, Revision, Member, ShiftExchange +from shifts.models import Shift, Revision, Member, ShiftExchange, ShiftForMarket from studies.models import StudyRequest from django.core.mail import send_mail @@ -53,6 +53,13 @@ class NotificationService(ABC): """ pass + @abstractmethod + def _triggerNotificationForMarket(self, event): + """ + triggers the notification on StudyRequest + """ + pass + def notify(self, event) -> int: if isinstance(event, (Shift, NewShiftsUpload, ShiftExchange)): return self._triggerShiftNotification(event) @@ -61,6 +68,8 @@ class NotificationService(ABC): return self._triggerRevisionNotification(event) elif isinstance(event, StudyRequest): return self._triggerStudyRequestNotification(event) + elif isinstance(event, ShiftForMarket): + return self._triggerNotificationForMarket(event) else: print("Do not know how to notify for ", event) return NOTIFICATION_CODE_ERROR @@ -80,6 +89,9 @@ class NewShiftsUpload: class DummyNotifier(NotificationService): """Mainly for test class""" + def _triggerNotificationForMarket(self, event): + pass + def __init__(self, name): super().__init__(name=name) @@ -184,6 +196,24 @@ class EmailNotifier(NotificationService): affectedMembersEmail=[one for one in affectedMembers if one.notification_shifts], ) + def _triggerNotificationForMarket(self, shiftForMarket: ShiftForMarket): + emailSubject = "[shifter] NEW shift on market" + emailBody = render_to_string( + "shift_market_email.html", + {"shiftForMarket": shiftForMarket, "domain": self.default_domain}, + ) + m = Member.objects.filter(role=shiftForMarket.shift.member.role) + affectedMembers = list(m) + + self._notify_internal_and_external( + actor=shiftForMarket.offerer, + target=shiftForMarket, + emailSubject=emailSubject, + emailBody=emailBody, + affectedMembers=affectedMembers, + affectedMembersEmail=[one for one in affectedMembers if one.notification_shifts], + ) + def _triggerRevisionNotification(self, revision): emailSubject = "[shifter] NEW planning ready for the preview" emailBody = render_to_string( diff --git a/shifts/migrations/0017_auto_20240905_1227.py b/shifts/migrations/0017_auto_20240905_1227.py index 2778970..31cbebb 100644 --- a/shifts/migrations/0017_auto_20240905_1227.py +++ b/shifts/migrations/0017_auto_20240905_1227.py @@ -5,20 +5,19 @@ import django.utils.timezone class Migration(migrations.Migration): - dependencies = [ - ('shifts', '0016_auto_20240904_0814'), + ("shifts", "0016_auto_20240904_0814"), ] operations = [ migrations.AddField( - model_name='shiftformarket', - name='available_until', + model_name="shiftformarket", + name="available_until", field=models.DateField(blank=True, null=True), ), migrations.AddField( - model_name='shiftformarket', - name='taken_date', + model_name="shiftformarket", + name="taken_date", field=models.DateTimeField(default=django.utils.timezone.now), ), ] diff --git a/shifts/migrations/0018_shiftformarket_urgency.py b/shifts/migrations/0018_shiftformarket_urgency.py index 1b681a7..d37af2c 100644 --- a/shifts/migrations/0018_shiftformarket_urgency.py +++ b/shifts/migrations/0018_shiftformarket_urgency.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('shifts', '0017_auto_20240905_1227'), + ("shifts", "0017_auto_20240905_1227"), ] operations = [ migrations.AddField( - model_name='shiftformarket', - name='urgency', - field=models.CharField(choices=[('Normal', 'Normal'), ('Urgent', 'Urgent')], default='Normal', max_length=10), + model_name="shiftformarket", + name="urgency", + field=models.CharField(choices=[("Normal", "Normal"), ("Urgent", "Urgent")], default="Normal", max_length=10), ), ] diff --git a/shifts/migrations/0019_alter_shiftformarket_taken_date.py b/shifts/migrations/0019_alter_shiftformarket_taken_date.py index ba92f9a..e3ef861 100644 --- a/shifts/migrations/0019_alter_shiftformarket_taken_date.py +++ b/shifts/migrations/0019_alter_shiftformarket_taken_date.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('shifts', '0018_shiftformarket_urgency'), + ("shifts", "0018_shiftformarket_urgency"), ] operations = [ migrations.AlterField( - model_name='shiftformarket', - name='taken_date', + model_name="shiftformarket", + name="taken_date", field=models.DateTimeField(blank=True, null=True), ), ] diff --git a/shifts/migrations/0020_shiftexchange_type.py b/shifts/migrations/0020_shiftexchange_type.py new file mode 100644 index 0000000..142f2a5 --- /dev/null +++ b/shifts/migrations/0020_shiftexchange_type.py @@ -0,0 +1,17 @@ +# Generated by Django 3.2.16 on 2024-09-05 20:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("shifts", "0019_alter_shiftformarket_taken_date"), + ] + + operations = [ + migrations.AddField( + model_name="shiftexchange", + name="type", + field=models.CharField(choices=[("Normal", "Normal"), ("Urgent", "Urgent"), ("Business", "Business")], default="Normal", max_length=10), + ), + ] diff --git a/shifts/models.py b/shifts/models.py index 1046565..91eb250 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -294,6 +294,10 @@ class Shift(models.Model): def shift_end(self) -> str: return self.get_proper_times(self.Moment.END).strftime(DATE_FORMAT_FULL) + @cached_property + def shift_day_of_week(self): + return self.date.strftime("%A") + def get_proper_times(self, moment) -> datetime: timeToUse = self.slot.hour_start if moment == self.Moment.END: @@ -327,7 +331,7 @@ class ShiftForMarket(models.Model): ordering = ["offered_date"] def __str__(self): - return f"Shift {self.shift} ordered {self.offerer} on {self.offered_date.strftime(DATE_FORMAT_FULL)}" + return f"Shift [{self.shift}] put by {self.offerer} on {self.offered_date.strftime(DATE_FORMAT_FULL)}" def mark_as_taken(self): self.is_available = False @@ -351,6 +355,18 @@ class ShiftForMarket(models.Model): "comments": self.comments, } + def get_market_shift_as_json_event(self) -> dict: + event = { + "id": self.id, + "title": f"{self.shift.slot.name} from Market [{self.urgency}]", + "start": self.shift.get_proper_times(self.shift.Moment.START).strftime(format=DATE_FORMAT_FULL), + "end": self.shift.get_proper_times(self.shift.Moment.END).strftime(format=DATE_FORMAT_FULL), + # FIXME add new link to treat the shift-market + "url": reverse("shifter:shift-view", kwargs={"sid": self.shift.id}), + "color": "#FF5733" if "Urgent" in self.urgency else "#ffb8a9", + } + return event + class ShiftExchangePair(models.Model): shift = models.ForeignKey(Shift, on_delete=DO_NOTHING) @@ -368,6 +384,7 @@ class ShiftExchangePair(models.Model): class ShiftExchange(models.Model): + EXCHANGE_TYPE = [("Normal", "Normal"), ("Urgent", "Urgent"), ("Business", "Business")] requestor = models.ForeignKey(Member, on_delete=DO_NOTHING) requested = models.DateTimeField() tentative = models.BooleanField(default=True) @@ -376,15 +393,15 @@ class ShiftExchange(models.Model): blank=True, null=True, ) - + type = models.CharField(max_length=10, choices=EXCHANGE_TYPE, default="Normal") shifts = models.ManyToManyField(ShiftExchangePair, blank=True) applicable = models.BooleanField(default=False) backupRevision = models.ForeignKey(Revision, on_delete=DO_NOTHING, related_name="revision") implemented = models.BooleanField(default=False) def __str__(self): - return "{} on {} for {} shifts swap; implemented:{}".format( - self.requestor, self.requested.strftime(SIMPLE_DATE), self.shifts.all().count(), self.implemented + return "[{}] {} on {} for {} shifts swap; implemented:{}".format( + self.type, self.requestor, self.requested.strftime(SIMPLE_DATE), self.shifts.all().count(), self.implemented ) @cached_property diff --git a/shifts/permanent_contexts.py b/shifts/permanent_contexts.py index bd04fec..eb2dd86 100644 --- a/shifts/permanent_contexts.py +++ b/shifts/permanent_contexts.py @@ -32,8 +32,10 @@ def useful_contact_context(request): def shift_market_count(request): - shift_count = ShiftForMarket.objects.filter(is_available=True, shift__member__role=request.user.role).count() - return {"shift_count": shift_count} + if request.user.is_authenticated: + shift_count = ShiftForMarket.objects.filter(is_available=True, shift__member__role=request.user.role).count() + return {"shift_count": shift_count} + return {} def nav_bar_context(request): diff --git a/shifts/static/js/calendar_script.js b/shifts/static/js/calendar_script.js index c3d0186..b29485f 100644 --- a/shifts/static/js/calendar_script.js +++ b/shifts/static/js/calendar_script.js @@ -89,6 +89,7 @@ $(document).ready(function () { $('#modalPost').html("Post shift comments : " + eventObj.extendedProps.post_comment); $('#eventUrl').attr('href',eventObj.url); $('#eventEdit').attr('href',"/shift/"+eventObj.id); + $('#eventView').attr('href',"/shift/"+eventObj.id+"/view"); $('#calendarModal').modal("show"); }, eventLimit: true, // allow 'more' link when too many events @@ -117,6 +118,14 @@ $(document).ready(function () { alert('there was an error while fetching events!'); }, + }, + { + id: "market", + url: $('#calendar').data('source-market-events'), + failure: function() { + alert('there was an error while fetching events!'); + }, + }, { id: "holidays", diff --git a/shifts/static/js/calendar_script.min.js b/shifts/static/js/calendar_script.min.js index 20cde60..ad90e81 100644 --- a/shifts/static/js/calendar_script.min.js +++ b/shifts/static/js/calendar_script.min.js @@ -1 +1 @@ -function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment),$("#eventUrl").attr("href",t.url),$("#eventEdit").attr("href","/shift/"+t.id),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); +function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment),$("#eventUrl").attr("href",t.url),$("#eventEdit").attr("href","/shift/"+t.id),$("#eventView").attr("href","/shift/"+t.id+"/view"),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"market",url:$("#calendar").data("source-market-events"),failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); diff --git a/shifts/templates/calendar.html b/shifts/templates/calendar.html index ba41e7e..e4c346a 100644 --- a/shifts/templates/calendar.html +++ b/shifts/templates/calendar.html @@ -3,6 +3,7 @@ <div class="col-lg-10"> <div id="calendar" style="padding-top: 10px;" data-source-shifts="{{event_source}}" + data-source-market-events="{{market_event_source}}" data-source-studies="{% url 'studies:ajax.get_studies' %}" data-source-holidays="{% url 'ajax.get_holidays' %}" data-source-team-events="{% url 'ajax.get_team_events' %}" @@ -25,8 +26,9 @@ </div> {%endif%} <div class="modal-footer"> - {%if request.user.is_staff %} <a id="eventEdit" type="button" class="btn btn-danger" data-dismiss="modal">Edit pre/post messages</a> {%endif%} - <a id="eventUrl" type="button" class="btn btn-success" data-dismiss="modal">Continue to shifter's schedule</a> + {%if request.user.is_staff or user_view %} <a id="eventEdit" type="button" class="btn btn-danger" data-dismiss="modal">Edit pre/post messages [ONLY FOR SHIFT]</a> {%endif%} + <a id="eventView" type="button" class="btn btn-success" data-dismiss="modal">View details of selected</a> + <a id="eventUrl" type="button" class="btn btn-success" data-dismiss="modal">Continue to overview</a> </div> </div> </div> diff --git a/shifts/templates/layout/navbar.html b/shifts/templates/layout/navbar.html index 67e6b81..896c6f6 100644 --- a/shifts/templates/layout/navbar.html +++ b/shifts/templates/layout/navbar.html @@ -70,6 +70,17 @@ </div> </a> </li> + <li class="nav-item position-relative"> + <a class="nav-link {% if request.resolver_match.url_name == 'shifts_market' %}active{% endif %}" href="{% url 'shifts_market' %}"> + <div class="d-flex flex-column align-items-center"> + <i class="fa-solid fa-store fa-1x"></i> + <span class="text-center">Shift Market</span> + {% if shift_count > 0 %} + <span class="badge bg-danger position-absolute top-0 start-100 translate-middle badge rounded-pill">{{ shift_count }}</span> + {% endif %} + </div> + </a> + </li> {% endif %} <!-- Rota-maker Link (Rota-maker role required) --> {% if rota_maker_for %} @@ -92,26 +103,6 @@ <li><a class="dropdown-item text-center" href="{% url 'shift-upload' %}">Upload planning</a></li> </ul> </li> - <li class="nav-item position-relative"> - <a class="nav-link {% if request.resolver_match.url_name == 'shifts_market' %}active{% endif %}" href="{% url 'shifts_market' %}"> - <div class="d-flex flex-column align-items-center"> - <i class="fa-solid fa-store fa-1x"></i> - <span class="text-center">Shift Market</span> - {% if shift_count > 0 %} - <span class="badge bg-danger position-absolute top-0 start-100 translate-middle badge rounded-pill">{{ shift_count }}</span> - <!-- bg-primary - Blue --> - <!-- bg-secondary - Gray --> - <!-- bg-success - Green --> - <!-- bg-danger - Red --> - <!-- bg-warning - Yellow --> - <!-- bg-info - Light Blue --> - <!-- bg-light - Light Gray --> - <!-- bg-dark - Black --> - <!-- bg-white - White --> - {% endif %} - </div> - </a> - </li> {% endif %} </ul> @@ -123,7 +114,7 @@ {% if user.is_authenticated %} <li class="nav-item dropdown dropstart"> <a class="nav-link dropdown-toggle d-flex align-items-center m-0 p-0 ps-3" href="#" id="navbarDropdownMenuLink" role="button" data-bs-toggle="dropdown" aria-expanded="false"> - <img class="rounded-circle" loading="lazy" height="35" src={% if shift.member.photo|length > 0 %} "data:image/gif;base64,{{shift.member.photo}}" {% else %} {% static 'img/anonymous.png' %} {% endif %} /> + <img class="rounded-circle" loading="lazy" height="35" src={% if request.user.photo|length > 0 %} "data:image/gif;base64,{{request.user.photo}}" {% else %} {% static 'img/anonymous.png' %} {% endif %} /> {% live_notify_badge badge_class="badge" %} </a> <ul class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink"> diff --git a/shifts/templates/shift_edit.html b/shifts/templates/shift_edit.html index 0ae9173..1cac863 100644 --- a/shifts/templates/shift_edit.html +++ b/shifts/templates/shift_edit.html @@ -1,15 +1,16 @@ {% extends 'template_main.html'%} {% block body %} -{%if request.user.is_staff %} +{%if not edit or request.user.is_staff or shift.member == request.user %} <div class="container mb-5 pb-5"> <div class="col"> <h2>{{shift}}</h2> + {% if request.user.is_staff and edit%} <form action="{% url 'shifter:shift-single-exchange-post' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} <div class="form-select bg-warning"> - <label for="shiftMember" class="form-label">Assigned shift member:</label> - <p>Note: Updating member will create an approved ShiftExchange with respective notifications!</p> + <h4>Re-assign shift member</h4> + <p>Note: Updating member will create an <strong>approved ShiftExchange</strong> with respective notifications!</p> <select class="form-select" name='shiftMember' id='shiftMember'> {% for sr in replacement %} <option value="{{sr.id}}" {%if shift.member == sr%} selected {%endif%}>Shifter - {{sr.name}}</option> @@ -21,10 +22,13 @@ </div> </div> </form> + {% endif %} + {% if edit %} <form action="{% url 'shifter:shift-edit-post' shift.id %}" method="POST" enctype="multipart/form-data" class="form-horizontal"> {% csrf_token %} <hr class="hr" /> <h4>Update current shift</h4> + Make an update to the shift, accoding to the need. <div class="form-select"> <label for="shiftRole" class="form-label">Assigned shift role:</label> <select class="form-select" name='shiftRole' id='shiftRole'> @@ -62,11 +66,14 @@ <button class="btn btn-primary">Update shift details</button> </div> </form> - - <div> </div> - - <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#addShiftToMarketModal"> - Add shift to market + {% endif %} + <div> + <hr/> + </div> + <h4>Submit to shift market</h4> + <p>Make it available for grab for others. Note: Please keep in mind that once transferred to others you may need to find other time to work the missing hours.</p> + <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="modal" data-bs-target="#addShiftToMarketModal"> + Add to market <i class="fa-solid fa-store fa-1x"></i> </button> <div class="modal fade" id="addShiftToMarketModal" tabindex="-1" aria-labelledby="addShiftToMarketModalLabel" aria-hidden="true"> diff --git a/shifts/templates/shift_market_email.html b/shifts/templates/shift_market_email.html new file mode 100644 index 0000000..b75ee3e --- /dev/null +++ b/shifts/templates/shift_market_email.html @@ -0,0 +1,13 @@ +Dear Shifter ({{shiftForMarket.shift.member.role}}), + +This is an automatic message regarding a new shift put on market. +Consult {{domain}}/shift-market for details. + +{{shiftForMarket}} + Comment: {{shiftForMarket.comments}} + Urgency: {{shiftForMarket.urgency}} + +Manage your notifications in {{ domain }}/user?notifications +Thanks! +-- +CR_OperationsGroup@ess.eu diff --git a/shifts/templates/shiftexchange_edit.html b/shifts/templates/shiftexchange_edit.html new file mode 100644 index 0000000..09b95c8 --- /dev/null +++ b/shifts/templates/shiftexchange_edit.html @@ -0,0 +1,19 @@ +{% extends 'template_main.html'%} +{% block body %} + +{%if request.user.is_staff %} +<div class="container mb-5 pb-5"> + <div class="col"> + <h2>{{shift}}</h2> + + {{sEx}} + + </div> +</div> +{%else%} +<div class="container mb-5 pb-5"> <h3>You do not have permission to navigate here! Contact OP group for further assistance.</h3></div> +{%endif%} +{% endblock %} + +{% block js %} +{% endblock %} diff --git a/shifts/templates/shifts_market.html b/shifts/templates/shifts_market.html index 386de84..2d85dab 100644 --- a/shifts/templates/shifts_market.html +++ b/shifts/templates/shifts_market.html @@ -5,83 +5,77 @@ {% block body %} <div class="container-fluid mb-5 pb-5"> <h1 class="text-center mb-3">Available Shifts on Market for {{request.user.role}}</h1> - <div class="list-group"> - {% for shift in available_shifts %} - <div class="list-group-item - {% if shift.urgency == 'Urgent' %} - border-danger - {% else %} - border-secondary - {% endif %} - "> - <h5 class="mb-1">{{ shift.shift }}</h5> - <p class="mb-1"><strong>Offered by:</strong> {{ shift.offerer }}</p> - <small><strong>Offered on:</strong> {{ shift.offered_date }}</small> - - <p><strong>Urgency:</strong> {{ shift.urgency }}</p> - - <p><strong>Available until:</strong> - {% if shift.available_until %} - {{ shift.available_until }} - {% else %} - Not specified - {% endif %} - </p> - - <p>{{ shift.comments }}</p> - - <div class="btn-group" role="group" aria-label="Shift actions"> - <a href="#" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#confirmTakeModal-{{ shift.id }}"> - Take - </a> - - {% if request.user == shift.offerer %} - <a href="#" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#confirmDeleteModal-{{ shift.id }}"> - Remove - </a> - {% endif %} - </div> - </div> - - <div class="modal fade" id="confirmTakeModal-{{ shift.id }}" tabindex="-1" aria-labelledby="confirmTakeLabel-{{ shift.id }}" aria-hidden="true"> + <div class="card-deck"> + {% for marketShift in available_shifts %} + <div class="modal fade" id="confirmTakeModal-{{ marketShift.id }}" tabindex="-1" aria-labelledby="confirmTakeLabel-{{ marketShift.id }}" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <h5 class="modal-title" id="confirmTakeLabel-{{ shift.id }}">Confirm Action</h5> + <h5 class="modal-title" id="confirmTakeLabel-{{ marketShift.id }}">Confirm Action</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> - Are you sure you want to take this shift? It will be available until {{ shift.available_until }}. + Are you sure you want to take this shift? It will be available until {{ marketShift.available_until }}. </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> - <a href="{% url 'take_shift' shift.id %}" class="btn btn-success">Confirm</a> + <a href="{% url 'take_shift' marketShift.id %}" class="btn btn-success">Confirm</a> </div> </div> </div> </div> - - <div class="modal fade" id="confirmDeleteModal-{{ shift.id }}" tabindex="-1" aria-labelledby="confirmDeleteLabel-{{ shift.id }}" aria-hidden="true"> + <div class="modal fade" id="confirmDeleteModal-{{ marketShift.id }}" tabindex="-1" aria-labelledby="confirmDeleteLabel-{{ marketShift.id }}" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> - <h5 class="modal-title" id="confirmDeleteLabel-{{ shift.id }}">Confirm Delete</h5> + <h5 class="modal-title" id="confirmDeleteLabel-{{ marketShift.id }}">Confirm Delete</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> - Are you sure you want to delete this shift? This action cannot be undone. The shift is available until {{ shift.available_until }}. + Are you sure you want to delete this shift? This action cannot be undone. The shift is available until {{ marketShift.available_until }}. </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> - <a href="{% url 'delete_shift' shift.id %}" class="btn btn-danger">Remove</a> + <a href="{% url 'delete_shift' marketShift.id %}" class="btn btn-danger">Remove</a> </div> </div> </div> </div> + <div class="card" style=" display: inline-block; width: 30rem; }"> + <h5 class="card-header {% if marketShift.urgency == 'Urgent' %}text-white bg-secondary mb-3{% endif %}"> + {{marketShift.shift.shift_day_of_week}}, {{ marketShift.shift.date }} + <br> {{ marketShift.shift.slot}} {% if marketShift.urgency == 'Urgent' %} + <i class="fa-solid fa-circle-exclamation"></i>{% endif %} </h5> + <div class="card-body"> + <p class="mb-2">Shift to take: {{ marketShift.shift }}</P> + <p class="mb-2"><strong>Offered by:</strong> {{ marketShift.offerer }} on {{ marketShift.offered_date }} </p> +<!-- <p class="mb-2">Status: {{ marketShift.urgency }}</p>--> + <p class="mb-2"><strong>Comment</strong>: {{ marketShift.comments }}</p> + </div> + <div class="card-footer"> + <p><strong>Available until:</strong> + {% if marketShift.available_until %} + {{ marketShift.available_until }} + {% else %} + Not specified + {% endif %} + </p> + <div class="btn-group" role="group" aria-label="Shift actions"> + <button href="#" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#confirmTakeModal-{{ marketShift.id }}"> + <i class="fa-solid fa-thumbs-up"></i> I take it! </button> {% if request.user == marketShift.offerer %} + <button href="#" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#confirmDeleteModal-{{ marketShift.id }}"> + <i class="fa-solid fa-trash fa-1x"></i> Remove from Market</button> + {% endif %} + </div> + </div> + </div> + + {% empty %} <p>No shifts available on the market at the moment.</p> {% endfor %} + </div> </div> {% endblock %} diff --git a/shifts/templates/user.html b/shifts/templates/user.html index 38588a5..77680bc 100644 --- a/shifts/templates/user.html +++ b/shifts/templates/user.html @@ -58,7 +58,7 @@ </ul> <div class="tab-content" id="user_tabs_content"> <div class="tab-pane fade show active" id="planning-tab-pane" role="tabpanel" aria-labelledby="planning-tab" tabindex=""0> - {% include 'calendar.html' with event_source=the_url %} + {% include 'calendar.html' with event_source=the_url market_event_source=the_market_url user_view=True%} </div> <!-- Tab displaying the time reporting breakdown --> <div class="tab-pane fade" id="time_report-tab-pane" role="tabpanel" aria-labelledby="time_report-tab" tabindex="1"> diff --git a/shifts/urls/ajax.py b/shifts/urls/ajax.py index 7799f58..ce133f4 100644 --- a/shifts/urls/ajax.py +++ b/shifts/urls/ajax.py @@ -21,5 +21,6 @@ urlpatterns = [ path("get_team_shift_inconsistencies", ajax_views.get_team_shift_inconsistencies, name="ajax.get_team_shift_inconsistencies"), path("get_stats", ajax_views.get_shift_stats, name="ajax.get_stats"), path("get_team_shiftswaps", ajax_views.get_team_shiftswaps, name="ajax.get_team_shiftswaps"), + path("get_market_events", ajax_views.get_market_events, name="ajax.get_market_events"), path("search", ajax_views.search, name="ajax.search"), ] diff --git a/shifts/urls/main.py b/shifts/urls/main.py index beecf86..df96d27 100644 --- a/shifts/urls/main.py +++ b/shifts/urls/main.py @@ -23,14 +23,17 @@ urlpatterns = [ path("scheduled-work-time", views.scheduled_work_time, name="scheduled_work_time"), path("shifts", views.shifts, name="shifts"), path("shift/<int:sid>", views.shift_edit, name="shift-edit"), + path("shift/<int:sid>/view", views.shift_view, name="shift-view"), path("shift/<int:sid>/edit", views.shift_edit_post, name="shift-edit-post"), - path("shifts_market/", views.shifts_market, name="shifts_market"), - path("planning/shift_market/take/<int:shift_id>/", views.take_shift, name="take_shift"), - path("shift_market/delete/<int:shift_id>/", views.delete_shift, name="delete_shift"), - path("shift/<int:shift_id>/add_shift_to_market/", views.add_shift_to_market, name="add_shift_to_market"), + path("shift-market/", views.market_shifts, name="shifts_market"), + path("shift-market/<int:shift_id>/view/", views.market_shift_view, name="market_shift_view"), + path("shift-market/<int:shift_id>/take/", views.market_shift_take, name="take_shift"), + path("shift-market/<int:shift_id>/delete/", views.market_shift_delete, name="delete_shift"), + path("shift/<int:shift_id>/add-to-market/", views.market_shift_add, name="add_shift_to_market"), path("shift/<int:sid>/exchange", views.shift_single_exchange_post, name="shift-single-exchange-post"), path("shift-exchange", views.shiftExchangeRequestCreateOrUpdate, name="shift-exchange-request"), path("shift-exchange/<int:ex_id>", views.shiftExchangeRequestCreateOrUpdate, name="shift-exchange-request"), + path("shift-exchange/<int:ex_id>/view", views.shiftExchangeView, name="shift-exchange-view"), path("shift-exchange/<int:ex_id>/close", views.shiftExchangeRequestClose, name="shift-exchange-close"), path("shift-exchange/<int:ex_id>/cancel", views.shiftExchangeRequestCancel, name="shift-exchange-cancel"), path("shift-exchange/<int:ex_id>/finalize", views.shiftExchangePerform, name="shift-exchange"), diff --git a/shifts/views/ajax.py b/shifts/views/ajax.py index 28523ec..d08651b 100644 --- a/shifts/views/ajax.py +++ b/shifts/views/ajax.py @@ -387,6 +387,14 @@ def get_shift_breakdown(request: HttpRequest) -> HttpResponse: ) +@login_required +def get_market_events(request: HttpRequest) -> HttpResponse: + # FIXME include due date filter + shifts_on_market = ShiftForMarket.objects.filter(is_available=True) + calendar_market_events = [d.get_market_shift_as_json_event() for d in shifts_on_market] + return HttpResponse(json.dumps(calendar_market_events), content_type="application/json") + + @login_required def get_shifts_for_exchange(request: HttpRequest) -> HttpResponse: member = _get_member(request) diff --git a/shifts/views/main.py b/shifts/views/main.py index f5f8c9f..d50c6d8 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -174,6 +174,7 @@ def user(request, u=None, rid=None): context["hide_extra_role_selection"] = True context["show_companion"] = True context["the_url"] = reverse("ajax.get_user_events") + context["the_market_url"] = reverse("ajax.get_market_events") context["unread_notifications"] = member.notifications.unread() if rid is not None: requested_revision = get_object_or_404(Revision, number=rid) @@ -258,6 +259,13 @@ def shiftExchangeRequestCancel(request, ex_id=None): return HttpResponseRedirect(reverse("shifter:user")) +@login_required +def shiftExchangeView(request, ex_id=None): + sEx = ShiftExchange.objects.get(id=ex_id) + context = {"sEx": sEx} + return render(request, "shiftexchange_edit.html", prepare_default_context(request, context)) + + @require_http_methods(["POST"]) @csrf_protect @login_required @@ -601,6 +609,16 @@ def shift_edit(request, sid=None): "shift": Shift.objects.get(id=sid), "shiftRoles": ShiftRole.objects.all(), "replacement": Member.objects.filter(team=request.user.team, is_active=True).order_by("role"), + "edit": True, + } + return render(request, "shift_edit.html", prepare_default_context(request, data)) + + +@require_safe +@login_required +def shift_view(request, sid=None): + data = { + "shift": Shift.objects.get(id=sid), } return render(request, "shift_edit.html", prepare_default_context(request, data)) @@ -640,7 +658,7 @@ def shift_edit_post(request, sid=None): @require_http_methods(["POST"]) @csrf_protect @login_required -def add_shift_to_market(request, shift_id): +def market_shift_add(request, shift_id): shift = get_object_or_404(Shift, id=shift_id) member = request.user @@ -666,16 +684,16 @@ def add_shift_to_market(request, shift_id): messages.error(request, "The 'available until' date cannot be in the past.") return redirect("shifter:user") - ShiftForMarket.objects.create( + sfm = ShiftForMarket.objects.create( shift=shift, offerer=member, offered_date=timezone.now(), comments=market_comment, available_until=available_until_date, urgency=urgency ) - + notificationService.notify(sfm) messages.success(request, "Shift successfully added to the market.") return redirect("shifter:shifts_market") @login_required -def shifts_market(request): +def market_shifts(request): user_role = request.user.role available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("offered_date") @@ -693,26 +711,42 @@ def shifts_market(request): @login_required -def take_shift(request, shift_id): +def market_shift_view(request, shift_id): + context = { + "available_shifts": [ShiftForMarket.objects.get(id=shift_id)], + } + return render(request, "shifts_market.html", context) + + +@login_required +def market_shift_take(request, shift_id): try: shift_market = ShiftForMarket.objects.get(id=shift_id, is_available=True) except ShiftForMarket.DoesNotExist: messages.error(request, "The shift you tried to take is no longer available.") return redirect("shifts_market") + # FIXME think if that should go inside the 'simplified exchange' + # for now, the 'my shift' on the same day are invalidated and put with comment + shifts = Shift.objects.filter(date=shift_market.shift.date, member=request.user, revision=Revision.objects.filter(valid=True).order_by("-number").first()) + for shift in shifts: + shift.is_active = False + shift.pre_comment = "Removed with the market shift: {}".format(shift_market) + shift.save() + sExDone = perform_simplified_exchange_and_save_backup( shift_market.shift, request.user, approver=request.user, revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first() ) - messages.success(request, "Requested shift exchange (update) is now successfully implemented.") notificationService.notify(sExDone) + shift_market.mark_as_taken() + messages.success(request, "Requested shift exchange (update) is now successfully implemented.") messages.success(request, "You have successfully taken the shift.") - shift_market.mark_as_taken() return redirect("shifts_market") @login_required -def delete_shift(request, shift_id): +def market_shift_delete(request, shift_id): shift_market = get_object_or_404(ShiftForMarket, id=shift_id) if request.user != shift_market.offerer: -- GitLab From 1ccd45bd0f960906c812a0bdf8491544fc993628 Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Sat, 7 Sep 2024 13:03:41 +0200 Subject: [PATCH 06/21] Fix for the link mess in calendar events --- shifts/models.py | 2 +- shifts/static/js/calendar_script.js | 13 +++++++++++-- shifts/static/js/calendar_script.min.js | 2 +- shifts/templates/calendar.html | 4 ++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/shifts/models.py b/shifts/models.py index 91eb250..5e8c720 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -362,7 +362,7 @@ class ShiftForMarket(models.Model): "start": self.shift.get_proper_times(self.shift.Moment.START).strftime(format=DATE_FORMAT_FULL), "end": self.shift.get_proper_times(self.shift.Moment.END).strftime(format=DATE_FORMAT_FULL), # FIXME add new link to treat the shift-market - "url": reverse("shifter:shift-view", kwargs={"sid": self.shift.id}), + "url": reverse("shifter:market_shift_view", kwargs={"shift_id": self.id}), "color": "#FF5733" if "Urgent" in self.urgency else "#ffb8a9", } return event diff --git a/shifts/static/js/calendar_script.js b/shifts/static/js/calendar_script.js index b29485f..8c4e393 100644 --- a/shifts/static/js/calendar_script.js +++ b/shifts/static/js/calendar_script.js @@ -88,8 +88,17 @@ $(document).ready(function () { $('#modalPre').html( "Pre shift comments : " + eventObj.extendedProps.pre_comment); $('#modalPost').html("Post shift comments : " + eventObj.extendedProps.post_comment); $('#eventUrl').attr('href',eventObj.url); - $('#eventEdit').attr('href',"/shift/"+eventObj.id); - $('#eventView').attr('href',"/shift/"+eventObj.id+"/view"); + if (eventObj.url.includes('market')) { + $('#eventUrl').text("See on market"); + $('#eventEdit').addClass("disabled"); + $('#eventView').addClass("disabled"); + } else { + $('#eventUrl').text("Continue to overview"); + $('#eventView').removeClass("disabled"); + $('#eventEdit').removeClass("disabled"); + $('#eventEdit').attr('href',"/shift/"+eventObj.id); + $('#eventView').attr('href',"/shift/"+eventObj.id+"/view"); + } $('#calendarModal').modal("show"); }, eventLimit: true, // allow 'more' link when too many events diff --git a/shifts/static/js/calendar_script.min.js b/shifts/static/js/calendar_script.min.js index ad90e81..52a1756 100644 --- a/shifts/static/js/calendar_script.min.js +++ b/shifts/static/js/calendar_script.min.js @@ -1 +1 @@ -function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment),$("#eventUrl").attr("href",t.url),$("#eventEdit").attr("href","/shift/"+t.id),$("#eventView").attr("href","/shift/"+t.id+"/view"),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"market",url:$("#calendar").data("source-market-events"),failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); +function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment),$("#eventUrl").attr("href",t.url),t.url.includes("market")?($("#eventUrl").text("See on market"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled")):($("#eventUrl").text("Continue to overview"),$("#eventView").removeClass("disabled"),$("#eventEdit").removeClass("disabled"),$("#eventEdit").attr("href","/shift/"+t.id),$("#eventView").attr("href","/shift/"+t.id+"/view")),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"market",url:$("#calendar").data("source-market-events"),failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); diff --git a/shifts/templates/calendar.html b/shifts/templates/calendar.html index e4c346a..70431a9 100644 --- a/shifts/templates/calendar.html +++ b/shifts/templates/calendar.html @@ -26,8 +26,8 @@ </div> {%endif%} <div class="modal-footer"> - {%if request.user.is_staff or user_view %} <a id="eventEdit" type="button" class="btn btn-danger" data-dismiss="modal">Edit pre/post messages [ONLY FOR SHIFT]</a> {%endif%} - <a id="eventView" type="button" class="btn btn-success" data-dismiss="modal">View details of selected</a> + {%if request.user.is_staff or user_view %} <a id="eventEdit" type="button" class="btn btn-danger" data-dismiss="modal">Edit</a> {%endif%} + <a id="eventView" type="button" class="btn btn-success" data-dismiss="modal">View details</a> <a id="eventUrl" type="button" class="btn btn-success" data-dismiss="modal">Continue to overview</a> </div> </div> -- GitLab From 99416bbb0cbee95829481a0a703a7e9c1f9f43f1 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Sun, 8 Sep 2024 08:23:11 +0200 Subject: [PATCH 07/21] final fix #75 --- .../0021_alter_shiftformarket_urgency.py | 18 ++++++++ .../0022_alter_shiftformarket_urgency.py | 18 ++++++++ shifts/models.py | 7 +++ shifts/templates/shift_edit.html | 46 ++++++++++++++++--- shifts/templates/shifts_market.html | 2 +- shifts/views/main.py | 38 +++++++++------ 6 files changed, 109 insertions(+), 20 deletions(-) create mode 100644 shifts/migrations/0021_alter_shiftformarket_urgency.py create mode 100644 shifts/migrations/0022_alter_shiftformarket_urgency.py diff --git a/shifts/migrations/0021_alter_shiftformarket_urgency.py b/shifts/migrations/0021_alter_shiftformarket_urgency.py new file mode 100644 index 0000000..be9fc51 --- /dev/null +++ b/shifts/migrations/0021_alter_shiftformarket_urgency.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2024-09-07 13:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shifts', '0020_shiftexchange_type'), + ] + + operations = [ + migrations.AlterField( + model_name='shiftformarket', + name='urgency', + field=models.CharField(blank=True, choices=[('Normal', 'Normal'), ('Urgent', 'Urgent')], default='Normal', max_length=10), + ), + ] diff --git a/shifts/migrations/0022_alter_shiftformarket_urgency.py b/shifts/migrations/0022_alter_shiftformarket_urgency.py new file mode 100644 index 0000000..c67caed --- /dev/null +++ b/shifts/migrations/0022_alter_shiftformarket_urgency.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2024-09-07 13:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shifts', '0021_alter_shiftformarket_urgency'), + ] + + operations = [ + migrations.AlterField( + model_name='shiftformarket', + name='urgency', + field=models.CharField(choices=[('Normal', 'Normal'), ('Urgent', 'Urgent')], default='Normal', max_length=10), + ), + ] diff --git a/shifts/models.py b/shifts/models.py index 5e8c720..ebac093 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -339,6 +339,13 @@ class ShiftForMarket(models.Model): self.taken_date = timezone.now() self.save() + def check_if_available(self, date): + print(self.available_until, date) + if self.available_until is not None: + if date > self.available_until: + self.is_available = False + self.save() + def mark_as_available(self): self.is_available = True self.is_taken = False diff --git a/shifts/templates/shift_edit.html b/shifts/templates/shift_edit.html index 1cac863..e9627aa 100644 --- a/shifts/templates/shift_edit.html +++ b/shifts/templates/shift_edit.html @@ -70,11 +70,45 @@ <div> <hr/> </div> - <h4>Submit to shift market</h4> - <p>Make it available for grab for others. Note: Please keep in mind that once transferred to others you may need to find other time to work the missing hours.</p> - <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="modal" data-bs-target="#addShiftToMarketModal"> - Add to market <i class="fa-solid fa-store fa-1x"></i> - </button> + <h4>Submit to shift market or Take shift</h4> + <p>Make it available for others or take it yourself. Note: Once transferred, you may need to find another time to work the missing hours.</p> + + {% if not on_market %} + <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="modal" data-bs-target="#addShiftToMarketModal"> + Add to market <i class="fa-solid fa-store fa-1x"></i> + + + </button> + {% else %} + <button type="button" class="btn btn-success btn-lg" data-bs-toggle="modal" data-bs-target="#takeShiftModal"> + Take shift <i class="fa-solid fa-check fa-1x"></i> + </button> + + <div class="modal fade" id="takeShiftModal" tabindex="-1" aria-labelledby="takeShiftModalLabel" aria-hidden="true"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title" id="takeShiftModalLabel">Confirm Shift Take</h5> + <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> + </div> + <div class="modal-body"> + <p>Are you sure you want to take this shift?</p> + <p><strong>Shift:</strong> {{ shift }}</p> + <p><strong>Date:</strong> {{ shift.date }}</p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> + <a href="{% url 'shifter:take_shift' on_market.id %}" class="btn btn-success">Confirm</a> + </div> + </div> + </div> + </div> + + + {% endif %} + + + <div class="modal fade" id="addShiftToMarketModal" tabindex="-1" aria-labelledby="addShiftToMarketModalLabel" aria-hidden="true"> <div class="modal-dialog"> @@ -92,7 +126,7 @@ </div> <div class="form-floating mt-3"> - <input type="date" class="form-control" name="availableUntil" id="availableUntil" required> + <input type="date" class="form-control" name="availableUntil" id="availableUntil"> <label for="availableUntil">Available Until</label> </div> diff --git a/shifts/templates/shifts_market.html b/shifts/templates/shifts_market.html index 2d85dab..7282833 100644 --- a/shifts/templates/shifts_market.html +++ b/shifts/templates/shifts_market.html @@ -42,7 +42,7 @@ </div> </div> - <div class="card" style=" display: inline-block; width: 30rem; }"> + <div class="card" style=" display: inline-block; width: 30rem; "> <h5 class="card-header {% if marketShift.urgency == 'Urgent' %}text-white bg-secondary mb-3{% endif %}"> {{marketShift.shift.shift_day_of_week}}, {{ marketShift.shift.date }} <br> {{ marketShift.shift.slot}} {% if marketShift.urgency == 'Urgent' %} diff --git a/shifts/views/main.py b/shifts/views/main.py index d50c6d8..8d8fe3c 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -605,8 +605,11 @@ def shifts_update_status_post(request): @require_safe @login_required def shift_edit(request, sid=None): + s = Shift.objects.get(id=sid) + sfm = ShiftForMarket.objects.filter(shift=s).first() data = { - "shift": Shift.objects.get(id=sid), + "shift": s, + "on_market": sfm, "shiftRoles": ShiftRole.objects.all(), "replacement": Member.objects.filter(team=request.user.team, is_active=True).order_by("role"), "edit": True, @@ -617,9 +620,9 @@ def shift_edit(request, sid=None): @require_safe @login_required def shift_view(request, sid=None): - data = { - "shift": Shift.objects.get(id=sid), - } + s = Shift.objects.get(id=sid) + sfm = ShiftForMarket.objects.filter(shift=s).first() + data = {"shift": s, "on_market": sfm} return render(request, "shift_edit.html", prepare_default_context(request, data)) @@ -674,18 +677,24 @@ def market_shift_add(request, shift_id): available_until = request.POST.get("availableUntil", None) urgency = request.POST.get("urgency", "Normal") - if not available_until: - messages.error(request, "Please select a valid 'available until' date.") - return redirect("shifter:user") + if available_until: + available_until_date = timezone.datetime.strptime(available_until, "%Y-%m-%d").date() - available_until_date = timezone.datetime.strptime(available_until, "%Y-%m-%d").date() - - if available_until_date < timezone.now().date(): - messages.error(request, "The 'available until' date cannot be in the past.") - return redirect("shifter:user") + if available_until_date < timezone.now().date(): + messages.error(request, "The 'available until' date cannot be in the past.") + return redirect("shifter:user") + else: + available_until_date = None sfm = ShiftForMarket.objects.create( - shift=shift, offerer=member, offered_date=timezone.now(), comments=market_comment, available_until=available_until_date, urgency=urgency + shift=shift, + offerer=member, + offered_date=timezone.now(), + comments=market_comment, + available_until=available_until_date, + urgency=urgency, + is_available=True, + is_taken=False, ) notificationService.notify(sfm) messages.success(request, "Shift successfully added to the market.") @@ -697,6 +706,9 @@ def market_shifts(request): user_role = request.user.role available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("offered_date") + for a in available_shifts: + a.check_if_available(timezone.now().date()) + available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("offered_date") shift_count = available_shifts.count() -- GitLab From 05681a90ca48767e0d1d62b0367269f8a8d74f20 Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Sun, 8 Sep 2024 09:50:49 +0200 Subject: [PATCH 08/21] Last fixes on Exchange views - fix on the exchange views link to individual - fix on the calendar click fro studies --- shifts/exchanges.py | 7 +++-- .../0021_alter_shiftformarket_urgency.py | 9 +++---- .../0022_alter_shiftformarket_urgency.py | 9 +++---- shifts/models.py | 5 ++-- shifts/static/js/calendar_script.js | 8 +++++- shifts/static/js/calendar_script.min.js | 2 +- shifts/static/js/shift_control.js | 5 +++- shifts/static/js/shift_control.min.js | 1 + shifts/static/js/team_shiftswaps.js | 17 ++++++++---- shifts/static/js/team_shiftswaps.min.js | 2 +- shifts/templates/shiftexchange_edit.html | 25 +++++++++++++++--- shifts/templates/team_desiderata.html | 16 ++++++++---- shifts/templates/user.html | 11 ++++---- shifts/views/ajax.py | 26 ++++++++++++++----- shifts/views/main.py | 14 +++++++--- studies/models.py | 1 + tests/test_shift_exchange.py | 2 +- 17 files changed, 112 insertions(+), 48 deletions(-) create mode 100644 shifts/static/js/shift_control.min.js diff --git a/shifts/exchanges.py b/shifts/exchanges.py index 5b511da..2fdfbca 100644 --- a/shifts/exchanges.py +++ b/shifts/exchanges.py @@ -153,14 +153,17 @@ def perform_exchange_and_save_backup(shiftExchange: ShiftExchange, approver: Mem return shiftsAfterSwap -def perform_simplified_exchange_and_save_backup(shift: Shift, newMember: Member, approver: Member, revisionBackup: Revision) -> ShiftExchange: +def perform_simplified_exchange_and_save_backup( + shift: Shift, newMember: Member, requestor: Member, approver: Member, revisionBackup: Revision, exType="Normal" +) -> ShiftExchange: """Performs a simplified shift exchange when in the existing shift new member is created""" fakeShift = _create_shift( shift, newMember, revision=revisionBackup, pre_comment="FakeAndTemporary shift created when updating the change of Member", permanent=True ) sPair = ShiftExchangePair.objects.create(shift=shift, shift_for_exchange=fakeShift) sEx = ShiftExchange() - sEx.requestor = approver + sEx.requestor = requestor + sEx.type = exType sEx.approver = approver sEx.backupRevision = revisionBackup sEx.requested = timezone.now() diff --git a/shifts/migrations/0021_alter_shiftformarket_urgency.py b/shifts/migrations/0021_alter_shiftformarket_urgency.py index be9fc51..c4ba664 100644 --- a/shifts/migrations/0021_alter_shiftformarket_urgency.py +++ b/shifts/migrations/0021_alter_shiftformarket_urgency.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('shifts', '0020_shiftexchange_type'), + ("shifts", "0020_shiftexchange_type"), ] operations = [ migrations.AlterField( - model_name='shiftformarket', - name='urgency', - field=models.CharField(blank=True, choices=[('Normal', 'Normal'), ('Urgent', 'Urgent')], default='Normal', max_length=10), + model_name="shiftformarket", + name="urgency", + field=models.CharField(blank=True, choices=[("Normal", "Normal"), ("Urgent", "Urgent")], default="Normal", max_length=10), ), ] diff --git a/shifts/migrations/0022_alter_shiftformarket_urgency.py b/shifts/migrations/0022_alter_shiftformarket_urgency.py index c67caed..6315fb2 100644 --- a/shifts/migrations/0022_alter_shiftformarket_urgency.py +++ b/shifts/migrations/0022_alter_shiftformarket_urgency.py @@ -4,15 +4,14 @@ from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ - ('shifts', '0021_alter_shiftformarket_urgency'), + ("shifts", "0021_alter_shiftformarket_urgency"), ] operations = [ migrations.AlterField( - model_name='shiftformarket', - name='urgency', - field=models.CharField(choices=[('Normal', 'Normal'), ('Urgent', 'Urgent')], default='Normal', max_length=10), + model_name="shiftformarket", + name="urgency", + field=models.CharField(choices=[("Normal", "Normal"), ("Urgent", "Urgent")], default="Normal", max_length=10), ), ] diff --git a/shifts/models.py b/shifts/models.py index ebac093..530b1e8 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -368,7 +368,6 @@ class ShiftForMarket(models.Model): "title": f"{self.shift.slot.name} from Market [{self.urgency}]", "start": self.shift.get_proper_times(self.shift.Moment.START).strftime(format=DATE_FORMAT_FULL), "end": self.shift.get_proper_times(self.shift.Moment.END).strftime(format=DATE_FORMAT_FULL), - # FIXME add new link to treat the shift-market "url": reverse("shifter:market_shift_view", kwargs={"shift_id": self.id}), "color": "#FF5733" if "Urgent" in self.urgency else "#ffb8a9", } @@ -380,7 +379,7 @@ class ShiftExchangePair(models.Model): shift_for_exchange = models.ForeignKey(Shift, on_delete=DO_NOTHING, related_name="for_exchange") def __str__(self): - return "[BEFORE] {} on {} {} with {} on {} {}".format( + return "[WAS] {} on {} {} with {} on {} {}".format( self.shift.member, self.shift.date, self.shift.slot.abbreviation, @@ -391,7 +390,7 @@ class ShiftExchangePair(models.Model): class ShiftExchange(models.Model): - EXCHANGE_TYPE = [("Normal", "Normal"), ("Urgent", "Urgent"), ("Business", "Business")] + EXCHANGE_TYPE = [("Normal", "Normal"), ("Market", "Market"), ("Business", "Business")] requestor = models.ForeignKey(Member, on_delete=DO_NOTHING) requested = models.DateTimeField() tentative = models.BooleanField(default=True) diff --git a/shifts/static/js/calendar_script.js b/shifts/static/js/calendar_script.js index 8c4e393..f844e04 100644 --- a/shifts/static/js/calendar_script.js +++ b/shifts/static/js/calendar_script.js @@ -92,7 +92,13 @@ $(document).ready(function () { $('#eventUrl').text("See on market"); $('#eventEdit').addClass("disabled"); $('#eventView').addClass("disabled"); - } else { + } + if (eventObj.url.includes('studies')) { + $('#eventUrl').text("Check the study details"); + $('#eventEdit').addClass("disabled"); + $('#eventView').addClass("disabled"); + } + else { $('#eventUrl').text("Continue to overview"); $('#eventView').removeClass("disabled"); $('#eventEdit').removeClass("disabled"); diff --git a/shifts/static/js/calendar_script.min.js b/shifts/static/js/calendar_script.min.js index 52a1756..b3386c3 100644 --- a/shifts/static/js/calendar_script.min.js +++ b/shifts/static/js/calendar_script.min.js @@ -1 +1 @@ -function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment),$("#eventUrl").attr("href",t.url),t.url.includes("market")?($("#eventUrl").text("See on market"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled")):($("#eventUrl").text("Continue to overview"),$("#eventView").removeClass("disabled"),$("#eventEdit").removeClass("disabled"),$("#eventEdit").attr("href","/shift/"+t.id),$("#eventView").attr("href","/shift/"+t.id+"/view")),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"market",url:$("#calendar").data("source-market-events"),failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); +function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment),$("#eventUrl").attr("href",t.url),t.url.includes("market")&&($("#eventUrl").text("See on market"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled")),t.url.includes("studies")?($("#eventUrl").text("Check the study details"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled")):($("#eventUrl").text("Continue to overview"),$("#eventView").removeClass("disabled"),$("#eventEdit").removeClass("disabled"),$("#eventEdit").attr("href","/shift/"+t.id),$("#eventView").attr("href","/shift/"+t.id+"/view")),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"market",url:$("#calendar").data("source-market-events"),failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); diff --git a/shifts/static/js/shift_control.js b/shifts/static/js/shift_control.js index 074d9b9..3c282ff 100644 --- a/shifts/static/js/shift_control.js +++ b/shifts/static/js/shift_control.js @@ -20,9 +20,12 @@ $(document).ready(function() { dom: 'P', bPaginate: false, paging: false, + searchPanes: { + initCollapsed: true + }, columns: [ { data: 'date', title: 'Day' }, - { data: 'members', title: 'Shifts' }, + { data: 'members', title: 'Crew' }, ], autoWidth: false, }); diff --git a/shifts/static/js/shift_control.min.js b/shifts/static/js/shift_control.min.js new file mode 100644 index 0000000..638e5e7 --- /dev/null +++ b/shifts/static/js/shift_control.min.js @@ -0,0 +1 @@ +$(document).ready(function(){$("#shiftControl_date_range_picker").daterangepicker({opens:"left"}),$("#shiftControl_date_range_picker").on("change",function(){$("#table_ShiftControl").DataTable().ajax.reload()}),$("#table_ShiftControl").DataTable({ajax:{url:$("#table_ShiftControl").data("source"),data:function(a){a.start=$("#shiftControl_date_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),a.end=$("#shiftControl_date_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD"),a.team=$("team_id").data("id")},dataSrc:"data"},dom:"P",bPaginate:!1,paging:!1,searchPanes:{initCollapsed:!0},columns:[{data:"date",title:"Day"},{data:"members",title:"Crew"},],autoWidth:!1})}); diff --git a/shifts/static/js/team_shiftswaps.js b/shifts/static/js/team_shiftswaps.js index 091f769..ce7e5c6 100644 --- a/shifts/static/js/team_shiftswaps.js +++ b/shifts/static/js/team_shiftswaps.js @@ -6,6 +6,7 @@ $(document).ready(function() { $('#shiftswap_date_range_picker').on('change', function() { $('#table_shiftswap').DataTable().ajax.reload(); }); + $('#table_shiftswap').DataTable({ ajax: { url: $('#table_shiftswap').data('source'), @@ -18,25 +19,31 @@ $(document).ready(function() { dom: 'P', bPaginate: false, paging: false, -// searchPanes: { -// initCollapsed: true -// }, - "columns": [{ + searchPanes: { + initCollapsed: true + }, + columns: [{ searchPanes: { show: false } }, + { // type + }, { searchPanes: { show: false } }, + { // name req + }, { -// visible:true, searchPanes: { show: false } }, + { // name approve + }, + { searchPanes: { show: false diff --git a/shifts/static/js/team_shiftswaps.min.js b/shifts/static/js/team_shiftswaps.min.js index 1c7156a..3c875e9 100644 --- a/shifts/static/js/team_shiftswaps.min.js +++ b/shifts/static/js/team_shiftswaps.min.js @@ -1 +1 @@ -$(document).ready(function(){$("#shiftswap_date_range_picker").daterangepicker({opens:"left"}),$("#shiftswap_date_range_picker").on("change",function(){$("#table_shiftswap").DataTable().ajax.reload()}),$("#table_shiftswap").DataTable({ajax:{url:$("#table_shiftswap").data("source"),data:function(a){a.start=$("#shiftswap_date_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),a.end=$("#shiftswap_date_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD"),a.team=$("team_id").data("id")}},dom:"P",bPaginate:!1,paging:!1,columns:[{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},{searchPanes:{show:!1}},],autoWidth:!1})}); +$(document).ready(function(){$("#shiftswap_date_range_picker").daterangepicker({opens:"left"}),$("#shiftswap_date_range_picker").on("change",function(){$("#table_shiftswap").DataTable().ajax.reload()}),$("#table_shiftswap").DataTable({ajax:{url:$("#table_shiftswap").data("source"),data:function(a){a.start=$("#shiftswap_date_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),a.end=$("#shiftswap_date_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD"),a.team=$("team_id").data("id")}},dom:"P",bPaginate:!1,paging:!1,searchPanes:{initCollapsed:!0},columns:[{searchPanes:{show:!1}},{},{searchPanes:{show:!1}},{},{searchPanes:{show:!1}},{},{searchPanes:{show:!1}},],autoWidth:!1})}); diff --git a/shifts/templates/shiftexchange_edit.html b/shifts/templates/shiftexchange_edit.html index 09b95c8..96a92b5 100644 --- a/shifts/templates/shiftexchange_edit.html +++ b/shifts/templates/shiftexchange_edit.html @@ -4,10 +4,27 @@ {%if request.user.is_staff %} <div class="container mb-5 pb-5"> <div class="col"> - <h2>{{shift}}</h2> - - {{sEx}} - + <div class="empty-div"></div> + <div class="card text-center"> + <div class="card-header"> + <span class="badge bg-dark">{{sEx.type}}</span> shift exchange. + </div> + <div class="card-body"> + <h5 class="card-title">Shift exchange details</h5> + <p class="card-text">Requested by {{sEx.requestor}} on {{sEx.requested}}</p> + <p class="card-text">The follwoing shifts were exchanged:</p> + <hr> + {% for onePair in sEx.shifts.all%} + <p> <small>[WAS]</small> {{onePair.shift}} <i class="fa-solid fa-arrows-left-right"></i> + <small>[IS now]</small> {{onePair.shift_for_exchange.member}}</p> + <p>[taker old shift] {{onePair.shift_for_exchange}}</p> + <hr> + {%endfor%} + </div> + <div class="card-footer text-muted"> + Approved on: {{sEx.approved}} by {{sEx.approver}} + </div> + </div> </div> </div> {%else%} diff --git a/shifts/templates/team_desiderata.html b/shifts/templates/team_desiderata.html index 787aae0..a8847ae 100644 --- a/shifts/templates/team_desiderata.html +++ b/shifts/templates/team_desiderata.html @@ -241,10 +241,13 @@ <table class="table table-striped" id="table_shiftswap" data-source="{% url 'ajax.get_team_shiftswaps' %}"> <thead> <tr> - <th scope="col">#</th> - <th scope="col">When</th> - <th scope="col">Who</th> - <th scope="col">Swap</th> + <th scope="col">Id</th> + <th scope="col">Type</th> + <th scope="col">When requested</th> + <th scope="col">Who requested</th> + <th scope="col">When finalised</th> + <th scope="col">Who finalised</th> + <th scope="col">Swapped shifts</th> </tr> </thead> <tbody> @@ -256,6 +259,9 @@ <th></th> <th></th> <th></th> + <th></th> + <th></th> + <th></th> </tr> </tfoot> </table> @@ -415,7 +421,7 @@ <script type="text/javascript" src="{% static 'js/desiderata_space.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/team_stats.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/team_shiftswaps.min.js' %}"></script> -<script type="text/javascript" src="{% static 'js/shift_control.js' %}"></script> +<script type="text/javascript" src="{% static 'js/shift_control.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/desiderata_gantt.min.js' %}"></script> diff --git a/shifts/templates/user.html b/shifts/templates/user.html index 77680bc..6be7fa5 100644 --- a/shifts/templates/user.html +++ b/shifts/templates/user.html @@ -303,7 +303,7 @@ <div class="accordion-item"> <h2 class="accordion-header" id="headingOne"> <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{oneSE.id}}" aria-expanded="true" aria-controls="collapse{{oneSE.id}}"> - {{oneSE.requestor}} <br> <small> requested on {{oneSE.requested_date}}</small> + <span class="badge text-bg-light">{{oneSE.type}}</span> {{oneSE.requestor}} <br> <small> on {{oneSE.requested_date}}</small> <span class="badge text-bg-{%if oneSE.implemented%}success">IMPLEMENTED{%else%}primary">NOT IMPLEMENTED{%endif%}</span> <span class="badge text-bg-{%if oneSE.applicable%}success">APPLICABLE{%else%}danger">NOT APPLICABLE{%endif%}</span> </button> @@ -319,14 +319,14 @@ {%endfor%} </dd> {%if oneSE.tentative%} - <a class="btn btn-outline-success btn-light" href={% url 'shifter:shift-exchange-close' oneSE.id %}>Close request and await for approval + <a class="btn btn-outline-success btn-light" href="{% url 'shifter:shift-exchange-close' oneSE.id %}">Close request and await for approval <span class="fa-stack"> <i class="fa-solid fa-circle fa-stack-2x" style="color:#198754"></i> <i class="fa-solid fa-bolt fa-stack-1x fa-inverse"></i> </span> </a> {%endif%} {%if not oneSE.implemented%} - <a class="btn btn-outline-danger btn-light" href={% url 'shifter:shift-exchange-cancel' oneSE.id %}>Cancel this request (cannot be undone!) + <a class="btn btn-outline-danger btn-light" href="{% url 'shifter:shift-exchange-cancel' oneSE.id %}">Cancel this request (cannot be undone!) <span class="fa-stack"> <i class="fa-solid fa-circle fa-stack-2x" ></i> <i class="fa-solid fa-trash fa-stack-1x fa-inverse"></i> @@ -336,12 +336,13 @@ <dl class="row"> <dd class="col-sm-1"> {%if oneSE.implemented%}<span class="fa-stack"> + <a href="{% url 'shifter:shift-exchange-view' oneSE.id %}"> <i class="fa-solid fa-circle fa-stack-2x" style="color:#198754">{{oneSE.approver.first_name}}</i> - <i class="fa-solid fa-thumbs-up fa-stack-1x fa-inverse"></i> + <i class="fa-solid fa-thumbs-up fa-stack-1x fa-inverse"></i></a> </span>{%endif%} {%if not oneSE.implemented and oneSE.applicable and not oneSE.tentative%} {%if oneSE.approver == member%} - <a class="btn btn-outline-success btn-light" href={% url 'shifter:shift-exchange' oneSE.id %}>Approve + <a class="btn btn-outline-success btn-light" href="{% url 'shifter:shift-exchange' oneSE.id %}">Approve <span class="fa-stack"> <i class="fa-solid fa-circle fa-stack-2x" style="color:#198754"></i> <i class="fa-solid fa-thumbs-up fa-stack-1x fa-inverse"></i> diff --git a/shifts/views/ajax.py b/shifts/views/ajax.py index d08651b..239e905 100644 --- a/shifts/views/ajax.py +++ b/shifts/views/ajax.py @@ -390,7 +390,7 @@ def get_shift_breakdown(request: HttpRequest) -> HttpResponse: @login_required def get_market_events(request: HttpRequest) -> HttpResponse: # FIXME include due date filter - shifts_on_market = ShiftForMarket.objects.filter(is_available=True) + shifts_on_market = ShiftForMarket.objects.filter(is_available=True, shift__member__role=request.user.role) calendar_market_events = [d.get_market_shift_as_json_event() for d in shifts_on_market] return HttpResponse(json.dumps(calendar_market_events), content_type="application/json") @@ -539,18 +539,32 @@ def get_team_shiftswaps(request: HttpRequest) -> HttpResponse: start = datetime.datetime.fromisoformat(request.GET.get("start")).date() end = datetime.datetime.fromisoformat(request.GET.get("end")).date() team = request.user.team - # TODO apply date filter on CONCERNED shifts (not requesting/approval times) - sExs = ShiftExchange.objects.filter(requestor__team=team, implemented=True) + sExs = ShiftExchange.objects.filter(implemented=True).filter( + (Q(requestor__team=team) | Q(shifts__shift_for_exchange__member__team=team) | Q(shifts__shift__member__team=team)) + & ( + Q(shifts__shift__date__gte=start) + & Q(shifts__shift__date__lte=end) + & Q(shifts__shift_for_exchange__date__gte=start) + & Q(shifts__shift_for_exchange__date__lte=end) + ) + ) + ids = [] data = [] for onesEx in sExs: + if onesEx.id in ids: + continue # poor's man remove duplicates + ids.append(onesEx.id) swaps = "" for oneSwap in onesEx.shifts.all(): swaps += str(oneSwap) + "<br>" data.append( [ - onesEx.id, - "Requested: " + onesEx.requested_date + " <br>Approved: " + onesEx.approved_date, - onesEx.requestor.name + " <br> " + onesEx.approver.name, + '<a class="link" href="' + reverse("shifter:shift-exchange-view", kwargs={"ex_id": onesEx.id}) + '">' + str(onesEx.id) + "</a>", + onesEx.type, + onesEx.requested_date, + onesEx.requestor.name, + onesEx.approved_date, + onesEx.approver.name, swaps, ] ) diff --git a/shifts/views/main.py b/shifts/views/main.py index 8d8fe3c..839b88d 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -186,7 +186,10 @@ def user(request, u=None, rid=None): messages.warning(request, "On top of the current schedule, you're seeing revision '{}'".format(requested_revision)) context["requested_future_rev_id"] = rid - shiftExchanges = ShiftExchange.objects.filter(Q(requestor=member) | Q(shifts__shift_for_exchange__member__exact=member)).order_by("-requested") + shiftExchanges = ShiftExchange.objects.filter( + Q(requestor=member) | Q(shifts__shift_for_exchange__member__exact=member) | Q(shifts__shift__member__exact=member) + ).order_by("-requested") + shiftExchangesLast = ShiftExchange.objects.filter(requestor=member, tentative=True, applicable=True).order_by("-requested").first() # FIXME this one is to clear the 'over select' from the query above that shiftExchangesUnique = [] @@ -747,7 +750,12 @@ def market_shift_take(request, shift_id): shift.save() sExDone = perform_simplified_exchange_and_save_backup( - shift_market.shift, request.user, approver=request.user, revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first() + shift_market.shift, + request.user, + requestor=shift_market.offerer, + approver=request.user, + revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first(), + exType="Market", ) notificationService.notify(sExDone) shift_market.mark_as_taken() @@ -787,7 +795,7 @@ def shift_single_exchange_post(request, sid=None): return HttpResponseRedirect(reverse("shifter:index")) sExDone = perform_simplified_exchange_and_save_backup( - s, m, approver=request.user, revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first() + s, m, requestor=request.user, approver=request.user, revisionBackup=Revision.objects.filter(name__startswith="BACKUP").first() ) messages.success(request, "Requested shift exchange (update) is now successfully implemented.") notificationService.notify(sExDone) diff --git a/studies/models.py b/studies/models.py index 02c95f5..7324a74 100644 --- a/studies/models.py +++ b/studies/models.py @@ -231,6 +231,7 @@ class StudyRequest(models.Model): "start": self.study_start, "end": self.study_end, "color": "#F5D959", + "url": reverse("studies:single_study_view", kwargs={"sid": self.id}), "textColor": "#FF3333" if self.priority else "#676767", "borderColor": "#FF3333" if self.priority else "#BBBBBB", } diff --git a/tests/test_shift_exchange.py b/tests/test_shift_exchange.py index a463594..85fa1ec 100644 --- a/tests/test_shift_exchange.py +++ b/tests/test_shift_exchange.py @@ -75,7 +75,7 @@ class ExchangeShifts(TestCase): def test_simple_exchange_just_member(self): self.assertEqual(self.m1, self.shift_m1_am.member) - perform_simplified_exchange_and_save_backup(self.shift_m1_am, self.m2, self.m2, revisionBackup=self.revisionBackup) + perform_simplified_exchange_and_save_backup(self.shift_m1_am, self.m2, self.m2, self.m2, revisionBackup=self.revisionBackup) aa = Shift.objects.filter(member=self.m2, date=self.shift_m1_am.date, slot=self.shift_m1_am.slot, revision=self.shift_m1_am.revision) self.assertEqual(1, len(aa)) -- GitLab From 9296c7637d5f118d166b6b509292ada5792dc777 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Sun, 8 Sep 2024 10:20:41 +0200 Subject: [PATCH 09/21] final final #75 --- shifts/models.py | 4 ++++ shifts/templates/shift_edit.html | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/shifts/models.py b/shifts/models.py index 530b1e8..2cac8d7 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -341,6 +341,10 @@ class ShiftForMarket(models.Model): def check_if_available(self, date): print(self.available_until, date) + if self: + if date > self.shift.date: + self.is_available = False + self.save() if self.available_until is not None: if date > self.available_until: self.is_available = False diff --git a/shifts/templates/shift_edit.html b/shifts/templates/shift_edit.html index e9627aa..a5b8459 100644 --- a/shifts/templates/shift_edit.html +++ b/shifts/templates/shift_edit.html @@ -76,10 +76,9 @@ {% if not on_market %} <button type="button" class="btn btn-secondary btn-lg" data-bs-toggle="modal" data-bs-target="#addShiftToMarketModal"> Add to market <i class="fa-solid fa-store fa-1x"></i> - - </button> {% else %} + {% if on_market.is_available %} <button type="button" class="btn btn-success btn-lg" data-bs-toggle="modal" data-bs-target="#takeShiftModal"> Take shift <i class="fa-solid fa-check fa-1x"></i> </button> @@ -103,6 +102,7 @@ </div> </div> </div> + {% endif %} {% endif %} -- GitLab From 6e2a7f61c563d223dc0f71ef219ef28cf99e3826 Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Sun, 8 Sep 2024 12:14:26 +0200 Subject: [PATCH 10/21] Add market validity checks - plus fix the ordering of market board --- shifts/exchanges.py | 100 +++++++++++++++++----------- shifts/models.py | 2 +- shifts/templates/shifts_market.html | 34 +++++++++- shifts/urls/ajax.py | 1 + shifts/views/ajax.py | 16 ++++- shifts/views/main.py | 4 +- 6 files changed, 110 insertions(+), 47 deletions(-) diff --git a/shifts/exchanges.py b/shifts/exchanges.py index 2fdfbca..73695a9 100644 --- a/shifts/exchanges.py +++ b/shifts/exchanges.py @@ -1,11 +1,11 @@ from django.db.models import Q -from shifts.models import ShiftExchange, Member, Shift, ShiftExchangePair, Revision +from shifts.models import ShiftExchange, Member, Shift, ShiftExchangePair, Revision, ShiftForMarket from shifts.workinghours import find_daily_rest_time_violation, find_weekly_rest_time_violation from datetime import timedelta from django.utils import timezone -def get_exchange_exchange_preview(shiftExchange: ShiftExchange, baseRevision, previewRangeDays=7, verbose=False): +def get_exchange_exchange_preview(shiftExchange: ShiftExchange, baseRevision, previewRangeDays=10, verbose=False): """ Returns all new shift plan wrt shifts requested for change (each change component queries previewRangeDays forward and backward) @@ -23,55 +23,62 @@ def get_exchange_exchange_preview(shiftExchange: ShiftExchange, baseRevision, pr print("\t", shiftExchange) allShiftsForNewPlanning = [] - usedSlots = [] + usedSlots = [] # these are needed prior to the main check! for shiftSwap in shiftExchange.shifts.all(): usedSlots += [shiftSwap.shift.id, shiftSwap.shift_for_exchange.id] for shiftSwap in shiftExchange.shifts.all(): - if verbose: - print("\t", shiftSwap) - # create new swapped shifts - newS = _prepare_after_swap_shifts(shiftSwap) - if verbose: - print("\t", newS) - allShiftsForNewPlanning += newS - # member 1 future setup: - sM1TmpBefore = Shift.objects.filter(member=shiftSwap.shift.member, revision=baseRevision).filter( - Q(date__lt=shiftSwap.shift_for_exchange.date) & Q(date__gt=(shiftSwap.shift_for_exchange.date + timedelta(days=-previewRangeDays))) - ) - sM1TmpAfter = Shift.objects.filter(member=shiftSwap.shift.member, revision=baseRevision).filter( - Q(date__gt=shiftSwap.shift_for_exchange.date) & Q(date__lt=(shiftSwap.shift_for_exchange.date + timedelta(days=previewRangeDays))) - ) - if verbose: - print("\t", sM1TmpBefore, sM1TmpAfter) - - for s in list(sM1TmpAfter) + list(sM1TmpBefore): - if s.id in usedSlots: - continue - usedSlots.append(s.id) - allShiftsForNewPlanning.append(s) - - sM2TmpBefore = Shift.objects.filter(member=shiftSwap.shift_for_exchange.member, revision=baseRevision).filter( - Q(date__lt=shiftSwap.shift.date) & Q(date__gt=(shiftSwap.shift.date + timedelta(days=-previewRangeDays))) - ) - sM2TmpAfter = Shift.objects.filter(member=shiftSwap.shift_for_exchange.member, revision=baseRevision).filter( - Q(date__gt=shiftSwap.shift.date) & Q(date__lt=(shiftSwap.shift.date + timedelta(days=previewRangeDays))) - ) - if verbose: - print("\t", sM2TmpBefore, sM2TmpAfter) - - for s in list(sM2TmpAfter) + list(sM2TmpBefore): - if s.id in usedSlots: - continue - usedSlots.append(s.id) - allShiftsForNewPlanning.append(s) + allShiftsForNewPlanning = _verifyForOneShiftSwap(shiftSwap, usedSlots, baseRevision, previewRangeDays=previewRangeDays, verbose=verbose) if verbose: print(allShiftsForNewPlanning) + print(shiftExchange.applicable) print("\t============================") return allShiftsForNewPlanning +def _verifyForOneShiftSwap(shiftSwap: ShiftExchangePair, usedSlots, baseRevision, previewRangeDays=10, verbose=False): + """ + Performs a virtual check for the + """ + allShiftsForNewPlanning = [] + if verbose: + print("\t", shiftSwap) + # create new swapped shifts + newS = _prepare_after_swap_shifts(shiftSwap) + if verbose: + print("\t", newS) + allShiftsForNewPlanning += newS + # member 1 future setup: + sM1TmpBefore = Shift.objects.filter(member=shiftSwap.shift.member, revision=baseRevision).filter( + Q(date__lt=shiftSwap.shift_for_exchange.date) & Q(date__gt=(shiftSwap.shift_for_exchange.date + timedelta(days=-previewRangeDays))) + ) + sM1TmpAfter = Shift.objects.filter(member=shiftSwap.shift.member, revision=baseRevision).filter( + Q(date__gt=shiftSwap.shift_for_exchange.date) & Q(date__lt=(shiftSwap.shift_for_exchange.date + timedelta(days=previewRangeDays))) + ) + if verbose: + print("\t", sM1TmpBefore, sM1TmpAfter) + for s in list(sM1TmpAfter) + list(sM1TmpBefore): + if s.id in usedSlots: + continue + usedSlots.append(s.id) + allShiftsForNewPlanning.append(s) + sM2TmpBefore = Shift.objects.filter(member=shiftSwap.shift_for_exchange.member, revision=baseRevision).filter( + Q(date__lt=shiftSwap.shift.date) & Q(date__gt=(shiftSwap.shift.date + timedelta(days=-previewRangeDays))) + ) + sM2TmpAfter = Shift.objects.filter(member=shiftSwap.shift_for_exchange.member, revision=baseRevision).filter( + Q(date__gt=shiftSwap.shift.date) & Q(date__lt=(shiftSwap.shift.date + timedelta(days=previewRangeDays))) + ) + if verbose: + print("\t", sM2TmpBefore, sM2TmpAfter) + for s in list(sM2TmpAfter) + list(sM2TmpBefore): + if s.id in usedSlots: + continue + usedSlots.append(s.id) + allShiftsForNewPlanning.append(s) + return allShiftsForNewPlanning + + def _create_shift(shift, member, revision=None, permanent=False, pre_comment=None): """ Creates a shift from a shift but with changed member and revision @@ -118,7 +125,20 @@ def is_valid_for_hours_constraints( # FIXME consider ShiftExchange per only TWO members, then one can save calls to check validity for each member s1 = find_daily_rest_time_violation([x for x in closeScheduleAfterUpdate if x.member == member]) s2 = find_weekly_rest_time_violation([x for x in closeScheduleAfterUpdate if x.member == member]) + return len(s1) == 0 & len(s2) == 0, (s1, s2) + + +def is_valid_for_hours_constraints_from_market(sfm: ShiftForMarket, revision: Revision, member: Member) -> tuple: + """ + returns tuple of boolean, and list of violated shifts + """ + shiftSwap = ShiftExchangePair(shift=sfm.shift, shift_for_exchange=_create_shift(sfm.shift, member)) + # print(sfm) + usedSlots = [shiftSwap.shift.id, shiftSwap.shift_for_exchange.id] + a = _verifyForOneShiftSwap(shiftSwap, usedSlots, revision, verbose=False) + s1 = find_daily_rest_time_violation([x for x in a if x.member == member]) + s2 = find_weekly_rest_time_violation([x for x in a if x.member == member]) return len(s1) == 0 & len(s2) == 0, (s1, s2) diff --git a/shifts/models.py b/shifts/models.py index 530b1e8..3c024be 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -340,7 +340,7 @@ class ShiftForMarket(models.Model): self.save() def check_if_available(self, date): - print(self.available_until, date) + # print(self.available_until, date) if self.available_until is not None: if date > self.available_until: self.is_available = False diff --git a/shifts/templates/shifts_market.html b/shifts/templates/shifts_market.html index 7282833..e9f356e 100644 --- a/shifts/templates/shifts_market.html +++ b/shifts/templates/shifts_market.html @@ -42,7 +42,7 @@ </div> </div> - <div class="card" style=" display: inline-block; width: 30rem; "> + <div id="market-card-{{marketShift.id}}" class="card" style=" display: inline-block; width: 23rem; "> <h5 class="card-header {% if marketShift.urgency == 'Urgent' %}text-white bg-secondary mb-3{% endif %}"> {{marketShift.shift.shift_day_of_week}}, {{ marketShift.shift.date }} <br> {{ marketShift.shift.slot}} {% if marketShift.urgency == 'Urgent' %} @@ -62,8 +62,11 @@ {% endif %} </p> <div class="btn-group" role="group" aria-label="Shift actions"> - <button href="#" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#confirmTakeModal-{{ marketShift.id }}"> - <i class="fa-solid fa-thumbs-up"></i> I take it! </button> {% if request.user == marketShift.offerer %} + <button id="take-{{marketShift.id}}" href="#" class="btn btn-success disabled" data-bs-toggle="modal" + data-bs-target="#confirmTakeModal-{{ marketShift.id }}"> <i class="fa-solid fa-thumbs-up"></i> I take it! + <span id="take-spinner-{{marketShift.id}}" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> + </button> + {% if request.user == marketShift.offerer %} <button href="#" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#confirmDeleteModal-{{ marketShift.id }}"> <i class="fa-solid fa-trash fa-1x"></i> Remove from Market</button> {% endif %} @@ -79,3 +82,28 @@ </div> </div> {% endblock %} + +{% block js %} +<script>{% for marketShift in available_shifts %} +$("#take-{{marketShift.id}}").ready(function () { + $.ajax({ + type: "GET", + url: '{% url 'ajax.check_market' %}?market_id={{marketShift.id}}', + success: function(res) { + $("#take-spinner-{{marketShift.id}}").removeClass("spinner-border"); + if (res.applicable){ + $("#take-{{marketShift.id}}").removeClass("disabled"); + } else { + $("#take-{{marketShift.id}}").removeClass("btn-success") + $("#take-{{marketShift.id}}").addClass("btn-danger") + $("#take-{{marketShift.id}}").addClass("btn-danger") + $("#take-{{marketShift.id}}").addClass("btn-danger") + $("#take-{{marketShift.id}}").text("Does not fit your schedule!") + $("#market-card-{{marketShift.id}}").addClass("text-secondary") + } + } + }); +}); +{%endfor%} +</script> +{% endblock %} diff --git a/shifts/urls/ajax.py b/shifts/urls/ajax.py index ce133f4..d8b9111 100644 --- a/shifts/urls/ajax.py +++ b/shifts/urls/ajax.py @@ -22,5 +22,6 @@ urlpatterns = [ path("get_stats", ajax_views.get_shift_stats, name="ajax.get_stats"), path("get_team_shiftswaps", ajax_views.get_team_shiftswaps, name="ajax.get_team_shiftswaps"), path("get_market_events", ajax_views.get_market_events, name="ajax.get_market_events"), + path("check_market", ajax_views.get_market_validity, name="ajax.check_market"), path("search", ajax_views.search, name="ajax.search"), ] diff --git a/shifts/views/ajax.py b/shifts/views/ajax.py index 239e905..a660ed0 100644 --- a/shifts/views/ajax.py +++ b/shifts/views/ajax.py @@ -3,6 +3,8 @@ from django.http import HttpRequest from django.db.models import Count, Q from django.contrib.auth.decorators import login_required from django.views.decorators.http import require_safe + +from shifts.exchanges import is_valid_for_hours_constraints_from_market from shifts.io import write_csv from shifts.models import * from studies.models import * @@ -389,12 +391,24 @@ def get_shift_breakdown(request: HttpRequest) -> HttpResponse: @login_required def get_market_events(request: HttpRequest) -> HttpResponse: - # FIXME include due date filter shifts_on_market = ShiftForMarket.objects.filter(is_available=True, shift__member__role=request.user.role) calendar_market_events = [d.get_market_shift_as_json_event() for d in shifts_on_market] return HttpResponse(json.dumps(calendar_market_events), content_type="application/json") +@login_required +def get_market_validity(request: HttpRequest) -> HttpResponse: + revision = _get_revision(request) + try: + sfm = ShiftForMarket.objects.get(id=request.GET.get("market_id")) + except: + return HttpResponse(json.dumps({"applicable": False, "info": "Wrong ID"}), content_type="application/json") + + is_applicable = is_valid_for_hours_constraints_from_market(sfm, revision, request.user) + # print(is_applicable) + return HttpResponse(json.dumps({"applicable": is_applicable[0]}), content_type="application/json") + + @login_required def get_shifts_for_exchange(request: HttpRequest) -> HttpResponse: member = _get_member(request) diff --git a/shifts/views/main.py b/shifts/views/main.py index 839b88d..2f63796 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -708,10 +708,10 @@ def market_shift_add(request, shift_id): def market_shifts(request): user_role = request.user.role - available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("offered_date") + available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("shift__date") for a in available_shifts: a.check_if_available(timezone.now().date()) - available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("offered_date") + available_shifts = ShiftForMarket.objects.filter(is_available=True, shift__member__role=user_role).order_by("shift__date") shift_count = available_shifts.count() -- GitLab From d1d5207a914daf3ddd9a7b591ff24b4f7e775c2f Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Mon, 9 Sep 2024 10:32:33 +0200 Subject: [PATCH 11/21] some color fix for desiderata plots (not finished) --- shifts/models.py | 8 +++++++- shifts/static/js/desiderata_gantt.js | 13 ++++++++++++- shifts/views/desiderata.py | 1 + 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/shifts/models.py b/shifts/models.py index b59cbae..42bd70b 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -153,7 +153,13 @@ class Desiderata(models.Model): ) def get_as_json_for_gantt(self): - return {"name": self.member.name, "start": int(timezone.localtime(self.start).timestamp()), "end": int(timezone.localtime(self.stop).timestamp())} + event = { + "name": self.member.name, + "role": self.member.role, + "start": int(timezone.localtime(self.start).timestamp()), + "end": int(timezone.localtime(self.stop).timestamp()) + } + return event def get_as_json_event(self, team=False, editable=True): event = { diff --git a/shifts/static/js/desiderata_gantt.js b/shifts/static/js/desiderata_gantt.js index e2ec417..191918b 100644 --- a/shifts/static/js/desiderata_gantt.js +++ b/shifts/static/js/desiderata_gantt.js @@ -38,6 +38,17 @@ function fill_gantt_plots() { url: $('#gantt-container').data('content_url'), data: { start: dStart, end: dEnd, team: teamId}, success: function(dataJSON) { + var roleColors = { + 'Operator': '#FF0000', + 'ShiftLeader': '#33FF57', + // добавьте другие роли и их цвета здеÑÑŒ + }; + + dataJSON.series.data.forEach(function(point) { + console.log(point); // добавьте Ð´Ð»Ñ Ð¾Ñ‚Ð»Ð°Ð´ÐºÐ¸ + point.color = roleColors[point.role] || '#000000'; // Цвет по умолчанию + }); + Highcharts.ganttChart('gantt-container', { title: { text: "Team Desiderata for overview" @@ -47,7 +58,7 @@ function fill_gantt_plots() { max: xAxisMax, labels: { formatter: function() { - var diffInMonths = (endDate.getFullYear() - startDate.getFullYear()) * 12 + endDate.getMonth() - startDate.getMonth() +1; + var diffInMonths = (endDate.getFullYear() - startDate.getFullYear()) * 12 + endDate.getMonth() - startDate.getMonth() + 1; if (diffInMonths < 6) { return Highcharts.dateFormat('W %W', this.value); } else { diff --git a/shifts/views/desiderata.py b/shifts/views/desiderata.py index c58819e..6e442c4 100644 --- a/shifts/views/desiderata.py +++ b/shifts/views/desiderata.py @@ -225,6 +225,7 @@ def gantt_data(request): toFix["y"] = ordered_names.index(d.member.name) toFix["start"] = int((d.start.timestamp()) + 24 * 60 * 60) * 1000 toFix["end"] = int(d.stop.timestamp() * 1000) + toFix["role"] = str(d.member.role) all_indexed_events.append(toFix) series["data"] = all_indexed_events -- GitLab From a1d50443b068163d27451a2edecc7f37d9619ef1 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Mon, 9 Sep 2024 14:20:11 +0200 Subject: [PATCH 12/21] import stady request #85 --- .../migrations/0016_alter_desiderata_type.py | 18 ++++++ studies/templates/request.html | 22 +++++++ studies/urls/main.py | 1 + studies/views/main.py | 63 +++++++++++++++++++ 4 files changed, 104 insertions(+) create mode 100644 shifts/migrations/0016_alter_desiderata_type.py diff --git a/shifts/migrations/0016_alter_desiderata_type.py b/shifts/migrations/0016_alter_desiderata_type.py new file mode 100644 index 0000000..ae559e6 --- /dev/null +++ b/shifts/migrations/0016_alter_desiderata_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2024-09-09 10:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('shifts', '0015_shiftexchange_shiftexchangepair'), + ] + + operations = [ + migrations.AlterField( + model_name='desiderata', + name='type', + field=models.CharField(choices=[('vac', 'Vacation'), ('conf', 'Conference'), ('wfh', 'Work From Home'), ('other', 'Other')], default='vac', max_length=10), + ), + ] diff --git a/studies/templates/request.html b/studies/templates/request.html index db0e994..46b5320 100644 --- a/studies/templates/request.html +++ b/studies/templates/request.html @@ -63,6 +63,28 @@ </div> </div> </div> + + <!-- Import Study Request List --> + <div class="accordion-item"> + <h2 class="accordion-header" id="import_request_header"> + <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#accordion_import_request" aria-expanded="false" aria-controls="accordion_import_request"> + Import Study Request + </button> + </h2> + <div id="accordion_import_request" class="accordion-collapse collapse" aria-labelledby="import_request_header" data-bs-parent="#assets_accordion"> + <div class="accordion-body"> + <form action="{% url 'studies:import_requests' %}" method="POST" enctype="multipart/form-data"> + {% csrf_token %} + <div class="mb-3"> + <label for="csv_file" class="form-label">Upload CSV File</label> + <input class="form-control" type="file" id="csv_file" name="csv_file" accept=".csv" required> + </div> + <input class="btn btn-primary" type="submit" value="Import"> + </form> + </div> + </div> + </div> + </div> </div> </div> diff --git a/studies/urls/main.py b/studies/urls/main.py index 97f8747..b6a2ff7 100644 --- a/studies/urls/main.py +++ b/studies/urls/main.py @@ -8,4 +8,5 @@ urlpatterns = [ path("", login_required(views.StudyView.as_view()), name="study_request"), path("<int:sid>", login_required(views.SingleStudyView.as_view()), name="single_study_view"), path("close", views.studies_close, name="studies-close"), + path("import-requests/", views.import_requests, name="import_requests"), ] diff --git a/studies/views/main.py b/studies/views/main.py index fc0f458..a1f9ceb 100644 --- a/studies/views/main.py +++ b/studies/views/main.py @@ -5,6 +5,9 @@ from studies.forms import StudyRequestForm, StudyRequestFormClosing import django.contrib.messages as messages from django.views import View from django.utils import timezone +import csv +from members.models import Member +from datetime import datetime from shifter.notifications import notificationService @@ -78,3 +81,63 @@ def studies_close(request): message = "Booking form is not valid, please correct." messages.success(request, message) return redirect("studies:study_request") + + +def import_requests(request): + def get_state_key(state_description): + state_mapping = {"Planned": "P", "Requested": "R", "Booked": "B", "Canceled": "C", "Done": "D"} + return state_mapping.get(state_description, None) + + def parse_date(date_str): + return datetime.strptime(date_str, DATE_FORMAT_FULL) + + if request.method == "POST": + csv_file = request.FILES.get("csv_file") + if csv_file: + data = csv_file.read().decode("utf-8").splitlines() + reader = csv.DictReader(data) + + for row in reader: + state_key = get_state_key(row["State"]) + if not state_key: + messages.error(request, f"Invalid state value: {row['State']} in CSV. Row skipped.") + continue + + booked_by_user = Member.objects.get(first_name=row["# name + bookedby"]) + if not booked_by_user: + messages.error(request, f"User {row['# name + bookedby']} not found. Skipping.") + continue + + slot_start = parse_date(row["slot start"]) + slot_end = parse_date(row["slot end"]) + + if not slot_start or not slot_end: + messages.error(request, f"Invalid date format for start or end time in row: {row}") + continue + + collaborators_names = row["Many users"].split(":") + collaborators = [] + + for name in collaborators_names: + collaborator = Member.objects.get(first_name=name.strip()) + if collaborator: + collaborators.append(collaborator) + + study_request = StudyRequest( + title=row["Title"], + description=row["Description"], + state=state_key, + booked_by=booked_by_user, + member=booked_by_user, + slot_start=slot_start, + slot_end=slot_end, + booking_created=timezone.now(), + ) + study_request.save() + if collaborators: + study_request.collaborators.set(collaborators) + + messages.success(request, "CSV file processed successfully.") + return redirect("studies:study_request") + + return render(request, "your_template.html") -- GitLab From 96d09e2439c04a97642aa01cdca4feff0a5d12c8 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Mon, 9 Sep 2024 14:42:31 +0200 Subject: [PATCH 13/21] fix #85 --- .../migrations/0016_alter_desiderata_type.py | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 shifts/migrations/0016_alter_desiderata_type.py diff --git a/shifts/migrations/0016_alter_desiderata_type.py b/shifts/migrations/0016_alter_desiderata_type.py deleted file mode 100644 index ae559e6..0000000 --- a/shifts/migrations/0016_alter_desiderata_type.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.16 on 2024-09-09 10:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('shifts', '0015_shiftexchange_shiftexchangepair'), - ] - - operations = [ - migrations.AlterField( - model_name='desiderata', - name='type', - field=models.CharField(choices=[('vac', 'Vacation'), ('conf', 'Conference'), ('wfh', 'Work From Home'), ('other', 'Other')], default='vac', max_length=10), - ), - ] -- GitLab From b95d3019ad3346623e59e262cc652657bfdece12 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Mon, 9 Sep 2024 14:54:24 +0200 Subject: [PATCH 14/21] fix2 #85 --- studies/templates/request.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/studies/templates/request.html b/studies/templates/request.html index 46b5320..69c773b 100644 --- a/studies/templates/request.html +++ b/studies/templates/request.html @@ -63,7 +63,7 @@ </div> </div> </div> - + {%if request.user.is_staff %} <!-- Import Study Request List --> <div class="accordion-item"> <h2 class="accordion-header" id="import_request_header"> @@ -84,7 +84,7 @@ </div> </div> </div> - + {%endif%} </div> </div> </div> -- GitLab From 06af801dd17e6734409ab2fdd11f2033a7f7c506 Mon Sep 17 00:00:00 2001 From: Serhii Zavalniuk <serhiizavalniuk@esss.se> Date: Tue, 10 Sep 2024 09:55:08 +0200 Subject: [PATCH 15/21] fix #85 --- shifts/templates/shifts_market.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/shifts/templates/shifts_market.html b/shifts/templates/shifts_market.html index e9f356e..0bbb8cc 100644 --- a/shifts/templates/shifts_market.html +++ b/shifts/templates/shifts_market.html @@ -15,7 +15,7 @@ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body"> - Are you sure you want to take this shift? It will be available until {{ marketShift.available_until }}. + Are you sure you want to take this shift? </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> @@ -84,7 +84,8 @@ {% endblock %} {% block js %} -<script>{% for marketShift in available_shifts %} +<script> +{% for marketShift in available_shifts %} $("#take-{{marketShift.id}}").ready(function () { $.ajax({ type: "GET", -- GitLab From 9de9c6edea4f415710993b7e0ea963ab616f0331 Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Tue, 10 Sep 2024 10:26:55 +0200 Subject: [PATCH 16/21] Fix the colors for gantt plot and Market admin display --- shifts/admin.py | 3 ++- shifts/models.py | 6 +++--- shifts/static/js/desiderata_gantt.js | 12 +++++------- shifts/static/js/desiderata_gantt.min.js | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/shifts/admin.py b/shifts/admin.py index 98d7fa2..9618b05 100644 --- a/shifts/admin.py +++ b/shifts/admin.py @@ -98,7 +98,8 @@ class ShiftIDAdmin(admin.ModelAdmin): @admin.register(ShiftForMarket) class ShiftForMarketAdmin(admin.ModelAdmin): model = ShiftForMarket - list_display = ["shift", "offerer", "offered_date", "is_available", "is_taken", "comments"] + list_display = ["shift", "offerer", "offered_date", "available_until", "is_available", "is_taken", "urgency", "comments"] + ordering = ("-offered_date",) @admin.register(Shift) diff --git a/shifts/models.py b/shifts/models.py index 42bd70b..a03bafc 100644 --- a/shifts/models.py +++ b/shifts/models.py @@ -155,9 +155,9 @@ class Desiderata(models.Model): def get_as_json_for_gantt(self): event = { "name": self.member.name, - "role": self.member.role, - "start": int(timezone.localtime(self.start).timestamp()), - "end": int(timezone.localtime(self.stop).timestamp()) + "role": self.member.role, + "start": int(timezone.localtime(self.start).timestamp()), + "end": int(timezone.localtime(self.stop).timestamp()), } return event diff --git a/shifts/static/js/desiderata_gantt.js b/shifts/static/js/desiderata_gantt.js index 191918b..379aff5 100644 --- a/shifts/static/js/desiderata_gantt.js +++ b/shifts/static/js/desiderata_gantt.js @@ -39,16 +39,14 @@ function fill_gantt_plots() { data: { start: dStart, end: dEnd, team: teamId}, success: function(dataJSON) { var roleColors = { - 'Operator': '#FF0000', - 'ShiftLeader': '#33FF57', - // добавьте другие роли и их цвета здеÑÑŒ + 'Operator': '#AFF359', + 'ShiftLeader': '#1338BE', }; - + dataJSON.series.data.forEach(function(point) { - console.log(point); // добавьте Ð´Ð»Ñ Ð¾Ñ‚Ð»Ð°Ð´ÐºÐ¸ - point.color = roleColors[point.role] || '#000000'; // Цвет по умолчанию + point.color = roleColors[point.role] || '#000000'; }); - + Highcharts.ganttChart('gantt-container', { title: { text: "Team Desiderata for overview" diff --git a/shifts/static/js/desiderata_gantt.min.js b/shifts/static/js/desiderata_gantt.min.js index a27d26a..e7f8389 100644 --- a/shifts/static/js/desiderata_gantt.min.js +++ b/shifts/static/js/desiderata_gantt.min.js @@ -1 +1 @@ -var dStart,dEnd;const currentYear=new Date().getFullYear();function fill_gantt_plots(){var t=$("team_id").data("id"),a=new Date(dStart),e=new Date(dEnd),r=Date.UTC(a.getFullYear(),a.getMonth(),a.getDate()),n=Date.UTC(e.getFullYear(),e.getMonth(),e.getDate());$.ajax({dataType:"json",method:"GET",url:$("#gantt-container").data("content_url"),data:{start:dStart,end:dEnd,team:t},success:function(t){Highcharts.ganttChart("gantt-container",{title:{text:"Team Desiderata for overview"},xAxis:{min:r,max:n,labels:{formatter:function(){return(e.getFullYear()-a.getFullYear())*12+e.getMonth()-a.getMonth()+1<6?Highcharts.dateFormat("W %W",this.value):Highcharts.dateFormat("%b",this.value)}}},yAxis:t.yAxis,series:[{name:t.series.Name,data:t.series.data,pointWidth:30}]})},error:function(){alert("There was an error while fetching stats!")}})}$(document).ready(function(){dStart=`${currentYear}-01-01`,dEnd=`${currentYear}-12-31`,$("#stat_desiderata_data_range_picker").daterangepicker({opens:"left"}),$("#stat_desiderata_data_range_picker").on("change",function(){dStart=$("#stat_desiderata_data_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),dEnd=$("#stat_desiderata_data_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD"),fill_gantt_plots()}),fill_gantt_plots()}); +var dStart,dEnd;const currentYear=new Date().getFullYear();function fill_gantt_plots(){var t=$("team_id").data("id"),a=new Date(dStart),e=new Date(dEnd),r=Date.UTC(a.getFullYear(),a.getMonth(),a.getDate()),n=Date.UTC(e.getFullYear(),e.getMonth(),e.getDate());$.ajax({dataType:"json",method:"GET",url:$("#gantt-container").data("content_url"),data:{start:dStart,end:dEnd,team:t},success:function(t){var d={Operator:"#AFF359",ShiftLeader:"#1338BE"};t.series.data.forEach(function(t){console.log(t),t.color=d[t.role]||"#000000"}),Highcharts.ganttChart("gantt-container",{title:{text:"Team Desiderata for overview"},xAxis:{min:r,max:n,labels:{formatter:function(){return(e.getFullYear()-a.getFullYear())*12+e.getMonth()-a.getMonth()+1<6?Highcharts.dateFormat("W %W",this.value):Highcharts.dateFormat("%b",this.value)}}},yAxis:t.yAxis,series:[{name:t.series.Name,data:t.series.data,pointWidth:30}]})},error:function(){alert("There was an error while fetching stats!")}})}$(document).ready(function(){dStart=`${currentYear}-01-01`,dEnd=`${currentYear}-12-31`,$("#stat_desiderata_data_range_picker").daterangepicker({opens:"left"}),$("#stat_desiderata_data_range_picker").on("change",function(){dStart=$("#stat_desiderata_data_range_picker").data("daterangepicker").startDate.format("YYYY-MM-DD"),dEnd=$("#stat_desiderata_data_range_picker").data("daterangepicker").endDate.format("YYYY-MM-DD"),fill_gantt_plots()}),fill_gantt_plots()}); -- GitLab From 2cae8c25a0f7d301f615e293eefdb6148e100c2e Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Tue, 10 Sep 2024 10:56:30 +0200 Subject: [PATCH 17/21] Unified RotaMaker buttons&actions --- shifts/templates/calendar.html | 3 ++- shifts/templates/layout/navbar.html | 2 +- shifts/templates/shift_edit.html | 2 +- shifts/templates/shifts_upload.html | 2 +- shifts/templates/team_desiderata.html | 6 +++--- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/shifts/templates/calendar.html b/shifts/templates/calendar.html index 70431a9..050f57a 100644 --- a/shifts/templates/calendar.html +++ b/shifts/templates/calendar.html @@ -26,7 +26,8 @@ </div> {%endif%} <div class="modal-footer"> - {%if request.user.is_staff or user_view %} <a id="eventEdit" type="button" class="btn btn-danger" data-dismiss="modal">Edit</a> {%endif%} + {%if request.user.is_staff or user_view %} <a id="eventEdit" type="button" class="btn btn-danger" data-dismiss="modal"> + <i class="fa-solid fa-user-tie fa-1x"></i>  <strong>EDIT</strong></a> {%endif%} <a id="eventView" type="button" class="btn btn-success" data-dismiss="modal">View details</a> <a id="eventUrl" type="button" class="btn btn-success" data-dismiss="modal">Continue to overview</a> </div> diff --git a/shifts/templates/layout/navbar.html b/shifts/templates/layout/navbar.html index 896c6f6..9b035fb 100644 --- a/shifts/templates/layout/navbar.html +++ b/shifts/templates/layout/navbar.html @@ -87,7 +87,7 @@ <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <div class="d-flex flex-column align-items-center"> - <i class="fa-solid fa-people-group fa-1x"></i> + <i class="fa-solid fa-user-tie fa-1x"></i> <span class="text-center">Rota-maker</span> </div> </a> diff --git a/shifts/templates/shift_edit.html b/shifts/templates/shift_edit.html index a5b8459..563cf3f 100644 --- a/shifts/templates/shift_edit.html +++ b/shifts/templates/shift_edit.html @@ -18,7 +18,7 @@ </select> <br> <div class="form-floating"> - <button class="btn btn-dark">Update shift member</button> + <button class="btn btn-danger"><i class="fa-solid fa-user-tie fa-1x"></i>  Update shift member</button> </div> </div> </form> diff --git a/shifts/templates/shifts_upload.html b/shifts/templates/shifts_upload.html index d9c8910..a2432e8 100644 --- a/shifts/templates/shifts_upload.html +++ b/shifts/templates/shifts_upload.html @@ -58,7 +58,7 @@ <small>Notify for available preview after successful upload (can be notified later from the admin panel)</small> </label> </div> - <button class="btn btn-primary"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span>Upload </button> + <button class="btn btn-primary"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span><i class="fa-solid fa-user-tie fa-1x"></i>  Upload </button> </form> </div> </div> diff --git a/shifts/templates/team_desiderata.html b/shifts/templates/team_desiderata.html index a8847ae..489563b 100644 --- a/shifts/templates/team_desiderata.html +++ b/shifts/templates/team_desiderata.html @@ -322,7 +322,7 @@ </div> <div class="mb-3"> <div class="col-md-3 col-sm-3 col-xs-12 col-md-offset-3" style="margin-bottom:10px;"> - <button class="btn btn-warning"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span>Update</button> + <button class="btn btn-warning"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span><i class="fa-solid fa-user-tie fa-1x"></i> Update</button> </div> </div> </form> @@ -356,7 +356,7 @@ </div> <div class="mb-3"> <div class="col-md-3 col-sm-3 col-xs-12 col-md-offset-3" style="margin-bottom:10px;"> - <button class="btn btn-danger"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span>Merge revisions</button> + <button class="btn btn-danger"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span><i class="fa-solid fa-user-tie fa-1x"></i> Merge revisions</button> </div> </div> </form> @@ -384,7 +384,7 @@ </div> <div class="mb-3"> <div class="col-md-3 col-sm-3 col-xs-12 col-md-offset-3" style="margin-bottom:10px;"> - <button class="btn btn-danger"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span>Remove</button> + <button class="btn btn-danger"> <span class="glyphicon glyphicon-upload" style="margin-right:5px;"></span><i class="fa-solid fa-user-tie fa-1x"></i> Remove</button> </div> </div> </form> -- GitLab From 20024455d85dd7969fd6860ebb8f0aff9a09b394 Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Tue, 10 Sep 2024 11:04:36 +0200 Subject: [PATCH 18/21] BugFix inconsistencies should be only on ACTIVE shifts. --- shifts/views/ajax.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shifts/views/ajax.py b/shifts/views/ajax.py index a660ed0..72ff2da 100644 --- a/shifts/views/ajax.py +++ b/shifts/views/ajax.py @@ -426,7 +426,7 @@ def get_shifts_for_exchange(request: HttpRequest) -> HttpResponse: def _get_inconsistencies_per_member(member, revision): # TODO fix it with proper js file to build it out of JSON # TODO once with json, return count of inconsistencies to enable badge - ss = Shift.objects.filter(revision=revision).filter(member=member) + ss = Shift.objects.filter(revision=revision, is_active=True).filter(member=member) dailyViolations = find_daily_rest_time_violation(scheduled_shifts=ss) weeklyViolations = find_weekly_rest_time_violation(scheduled_shifts=ss) toReturnHTML = "" -- GitLab From cb36e225b14a1e02632390f6dc10630b53529716 Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Tue, 10 Sep 2024 11:11:27 +0200 Subject: [PATCH 19/21] bugfix add re-perform compatibility check on take action --- shifts/views/main.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/shifts/views/main.py b/shifts/views/main.py index 2f63796..342df84 100644 --- a/shifts/views/main.py +++ b/shifts/views/main.py @@ -23,7 +23,12 @@ from shifts.activeshift import prepare_active_crew, prepare_for_JSON from shifts.contexts import prepare_default_context, prepare_user_context from shifter.settings import DEFAULT_SHIFT_SLOT from shifts.workinghours import find_working_hours -from shifts.exchanges import is_valid_for_hours_constraints, perform_exchange_and_save_backup, perform_simplified_exchange_and_save_backup +from shifts.exchanges import ( + is_valid_for_hours_constraints, + perform_exchange_and_save_backup, + perform_simplified_exchange_and_save_backup, + is_valid_for_hours_constraints_from_market, +) from shifts.io import importShiftsFromCSV from django.utils import timezone @@ -741,6 +746,11 @@ def market_shift_take(request, shift_id): messages.error(request, "The shift you tried to take is no longer available.") return redirect("shifts_market") + is_applicable = is_valid_for_hours_constraints_from_market(shift_market, shift_market.shift.revision, request.user) + if not is_applicable[0]: + messages.error(request, "Your schedule does not fit to accept this shift! Contact your rota maker to find out " "possible solutions!") + return redirect("shifts_market") + # FIXME think if that should go inside the 'simplified exchange' # for now, the 'my shift' on the same day are invalidated and put with comment shifts = Shift.objects.filter(date=shift_market.shift.date, member=request.user, revision=Revision.objects.filter(valid=True).order_by("-number").first()) -- GitLab From cc09d1c738d8cb38220fe33c194dedc9ef6fe5f4 Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Tue, 10 Sep 2024 11:50:48 +0200 Subject: [PATCH 20/21] BugFix javascript for calendar buttons and style for the upload form --- shifts/static/js/calendar_script.js | 10 +++++++--- shifts/static/js/calendar_script.min.js | 2 +- shifts/templates/shifts_upload.html | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/shifts/static/js/calendar_script.js b/shifts/static/js/calendar_script.js index f844e04..35ca768 100644 --- a/shifts/static/js/calendar_script.js +++ b/shifts/static/js/calendar_script.js @@ -85,18 +85,20 @@ $(document).ready(function () { info.jsEvent.preventDefault(); var eventObj = info.event; $('#modalTitle').html(eventObj.title + " for " + eventObj.extendedProps.slot + " on " + eventObj.start); - $('#modalPre').html( "Pre shift comments : " + eventObj.extendedProps.pre_comment); - $('#modalPost').html("Post shift comments : " + eventObj.extendedProps.post_comment); $('#eventUrl').attr('href',eventObj.url); if (eventObj.url.includes('market')) { $('#eventUrl').text("See on market"); $('#eventEdit').addClass("disabled"); $('#eventView').addClass("disabled"); + $('#modalPre').text(""); + $('#modalPost').text(""); } - if (eventObj.url.includes('studies')) { + else if (eventObj.url.includes('studies')) { $('#eventUrl').text("Check the study details"); $('#eventEdit').addClass("disabled"); $('#eventView').addClass("disabled"); + $('#modalPre').text(""); + $('#modalPost').text(""); } else { $('#eventUrl').text("Continue to overview"); @@ -104,6 +106,8 @@ $(document).ready(function () { $('#eventEdit').removeClass("disabled"); $('#eventEdit').attr('href',"/shift/"+eventObj.id); $('#eventView').attr('href',"/shift/"+eventObj.id+"/view"); + $('#modalPre').html( "Pre shift comments : " + eventObj.extendedProps.pre_comment); + $('#modalPost').html("Post shift comments : " + eventObj.extendedProps.post_comment); } $('#calendarModal').modal("show"); }, diff --git a/shifts/static/js/calendar_script.min.js b/shifts/static/js/calendar_script.min.js index b3386c3..d9c6607 100644 --- a/shifts/static/js/calendar_script.min.js +++ b/shifts/static/js/calendar_script.min.js @@ -1 +1 @@ -function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment),$("#eventUrl").attr("href",t.url),t.url.includes("market")&&($("#eventUrl").text("See on market"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled")),t.url.includes("studies")?($("#eventUrl").text("Check the study details"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled")):($("#eventUrl").text("Continue to overview"),$("#eventView").removeClass("disabled"),$("#eventEdit").removeClass("disabled"),$("#eventEdit").attr("href","/shift/"+t.id),$("#eventView").attr("href","/shift/"+t.id+"/view")),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"market",url:$("#calendar").data("source-market-events"),failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); +function get_selected_campaigns(){return $(".displayed_campaigns").val()}function get_revision(){return null!==document.getElementById("displayed_revision")?$(".displayed_revision").val():-1}function get_revision_next(){return $("[name='future_revisions_checkboxes']").length?$("input[name='future_revisions_checkboxes']:checked").data("future_rev_id"):-1}function get_specific_users(){return null!==document.getElementById("users_selection")?$(".users_selection").val():-1}function get_team_id(){let e=$("#team_id_for_ajax");return e.length?e.data("id"):-1}function get_member_id(){let e=$("member_id");return e.length?e.data("id"):-1}$(document).ready(function(){let e=document.getElementById("calendar"),t=new FullCalendar.Calendar(e,{themeSystem:"bootstrap5",contentHeight:"auto",customButtons:{myCustomButton:{text:"Tools",click:function(){myOffcanvas=$("#tools_off_canvas"),new bootstrap.Offcanvas(myOffcanvas).show()}}},headerToolbar:{left:"prev,today,next",center:"title",right:"myCustomButton dayGridMonth,timeGridWeek"},columnFormat:{month:"ddd",week:"ddd M/d"},initialDate:$("#calendar").data("default-date"),weekNumbers:!0,navLinks:!0,editable:!1,firstDay:1,businessHours:[{daysOfWeek:[1,2,3,4,5],startTime:"08:00",endTime:"18:00"},],eventClick:function(e){e.jsEvent.preventDefault();var t=e.event;$("#modalTitle").html(t.title+" for "+t.extendedProps.slot+" on "+t.start),$("#eventUrl").attr("href",t.url),t.url.includes("market")?($("#eventUrl").text("See on market"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled"),$("#modalPre").text(""),$("#modalPost").text("")):t.url.includes("studies")?($("#eventUrl").text("Check the study details"),$("#eventEdit").addClass("disabled"),$("#eventView").addClass("disabled"),$("#modalPre").text(""),$("#modalPost").text("")):($("#eventUrl").text("Continue to overview"),$("#eventView").removeClass("disabled"),$("#eventEdit").removeClass("disabled"),$("#eventEdit").attr("href","/shift/"+t.id),$("#eventView").attr("href","/shift/"+t.id+"/view"),$("#modalPre").html("Pre shift comments : "+t.extendedProps.pre_comment),$("#modalPost").html("Post shift comments : "+t.extendedProps.post_comment)),$("#calendarModal").modal("show")},eventLimit:!0,eventDisplay:"block",eventOrder:"start,id,name,title",eventSources:[{id:"shifts",url:$("#calendar").data("source-shifts"),extraParams:function(){return{all_roles:$("#all_roles").is(":checked"),all_states:$("#all_states").is(":checked"),companion:$("#show_companion").is(":checked"),revision:get_revision(),revision_next:get_revision_next(),campaigns:get_selected_campaigns(),team:get_team_id(),member:get_member_id(),users:get_specific_users()}},failure:function(){alert("there was an error while fetching events!")}},{id:"market",url:$("#calendar").data("source-market-events"),failure:function(){alert("there was an error while fetching events!")}},{id:"holidays",url:$("#calendar").data("source-holidays"),failure:function(){alert("there was an error while fetching public holidays!")}},{id:"teamevents",url:$("#calendar").data("source-team-events"),failure:function(){alert("there was an error while fetching team-events dates!")}},{id:"studies",url:$("#calendar").data("source-studies"),extraParams:function(){return{show_studies:$("#show_studies").is(":checked"),team:get_team_id(),member:get_member_id()}},failure:function(){alert("there was an error while fetching studies planning!")}}]});t.render(),$("#all_roles").change(function(){t.getEventSourceById("shifts").refetch()}),$("#all_states").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_companion").change(function(){t.getEventSourceById("shifts").refetch()}),$("#show_studies").change(function(){t.getEventSourceById("studies").refetch()}),$(".users_selection").change(function(){t.getEventSourceById("shifts").refetch()}),$(".displayed_campaigns").change(function(){t.getEventSourceById("shifts").refetch()}),$("#planning-tab").click(function(){t.render()}),$("input[type=radio][name=future_revisions_checkboxes]").change(function(){t.getEventSourceById("shifts").refetch()})}); diff --git a/shifts/templates/shifts_upload.html b/shifts/templates/shifts_upload.html index a2432e8..b5f1917 100644 --- a/shifts/templates/shifts_upload.html +++ b/shifts/templates/shifts_upload.html @@ -15,7 +15,7 @@ {% csrf_token %} <div class="mb-3"> <label for="csv_file" class="form-label">CSV File: </label> - <input class="form-control-file" type="file" name="csv_file" id="csv_file" required> + <input class="form-control" type="file" name="csv_file" id="csv_file" required> </div> <div class="mb-3"> <label for="camp" class="form-label">Campaign: </label> -- GitLab From bc50065647d6d7aae228f533e7dadb9291652b1d Mon Sep 17 00:00:00 2001 From: arek <arek.gorzawski@ess.eu> Date: Mon, 23 Sep 2024 19:43:41 +0200 Subject: [PATCH 21/21] updated changelog --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0abfe23..52f9e46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # ESS OP Shifter Changelog +### v.1.1.4 +- Introduced the Shift market +- ShiftSwap Summary log with filters and details +- [bug fixes] notification email format correction +- [bug fixes] user icon display +- [bug fixes] date filters for shift inconsistency + +### v.1.1.3 +- [bug fixes] using only active users for desiderata +- [bug fixes] plot display issues +- [bug fixes] import messages + ### v.1.1.2 - Rota maker improvements - desideratas overview -- GitLab