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> &nbsp; </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> &nbsp; </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>&nbsp;requested on {{oneSE.requested_date}}</small>&nbsp;
+                                        <span class="badge text-bg-light">{{oneSE.type}}</span>&nbsp;&nbsp;{{oneSE.requestor}} <br> <small>&nbsp;on {{oneSE.requested_date}}</small>&nbsp;
                                           <span class="badge text-bg-{%if oneSE.implemented%}success">IMPLEMENTED{%else%}primary">NOT IMPLEMENTED{%endif%}</span>&nbsp;
                                           <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>&nbsp&nbsp<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>&nbsp 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>&nbsp 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>&nbspUpdate</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>&nbspMerge 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>&nbspRemove</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