diff --git a/AKModel/availability/models.py b/AKModel/availability/models.py index 94f0402f66346f4836a1c0a566ccf8b27f8e8fc8..b44cf9bda19e51aa287f9c34ce116c9c3a55f2b1 100644 --- a/AKModel/availability/models.py +++ b/AKModel/availability/models.py @@ -231,6 +231,10 @@ class Availability(models.Model): result = cls._pair_intersection(result, availset) return result + @property + def simplified(self): + return f'{self.start.astimezone(self.event.timezone).strftime("%a %H:%M")}-{self.end.astimezone(self.event.timezone).strftime("%a %H:%M")}' + class Meta: verbose_name = _('Availability') verbose_name_plural = _('Availabilities') diff --git a/AKModel/migrations/0042_akslot_fixed.py b/AKModel/migrations/0042_akslot_fixed.py new file mode 100644 index 0000000000000000000000000000000000000000..c33ee8f394d64a7705368549416f4a5432c97ede --- /dev/null +++ b/AKModel/migrations/0042_akslot_fixed.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.6 on 2020-11-04 23:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('AKModel', '0041_constraint_violation'), + ] + + operations = [ + migrations.AddField( + model_name='akslot', + name='fixed', + field=models.BooleanField(default=False, help_text='Length and time of this AK should not be changed', verbose_name='Scheduling Fixed'), + ), + ] diff --git a/AKModel/models.py b/AKModel/models.py index d247fbb7632e030bc28656e2f0437e2f5fc038a6..e581f00d2679cd7972540eca0d543384ddde1d94 100644 --- a/AKModel/models.py +++ b/AKModel/models.py @@ -25,7 +25,7 @@ class Event(models.Model): start = models.DateTimeField(verbose_name=_('Start'), help_text=_('Time the event begins')) end = models.DateTimeField(verbose_name=_('End'), help_text=_('Time the event ends')) reso_deadline = models.DateTimeField(verbose_name=_('Resolution Deadline'), blank=True, null=True, - help_text=_('When should AKs with intention to submit a resolution be done?')) + help_text=_('When should AKs with intention to submit a resolution be done?')) public = models.BooleanField(verbose_name=_('Public event'), default=True, help_text=_('Show this event on overview page.')) @@ -247,13 +247,16 @@ class AK(models.Model): @property def details(self): + from AKModel.availability.models import Availability + availabilities = ', \n'.join(f'{a.simplified}' for a in Availability.objects.filter(ak=self)) return f"""{self.name}{" (R)" if self.reso else ""}: {self.owners_list} {_("Requirements")}: {", ".join(str(r) for r in self.requirements.all())} {_("Conflicts")}: {", ".join(str(c) for c in self.conflicts.all())} - {_("Prerequisites")}: {", ".join(str(p) for p in self.prerequisites.all())}""" + {_("Prerequisites")}: {", ".join(str(p) for p in self.prerequisites.all())} + {_("Availabilities")}: \n{availabilities}""" @property def owners_list(self): @@ -316,6 +319,8 @@ class AKSlot(models.Model): duration = models.DecimalField(max_digits=4, decimal_places=2, default=2, verbose_name=_('Duration'), help_text=_('Length in hours')) + fixed = models.BooleanField(default=False, verbose_name=_('Scheduling Fixed'), help_text=_('Length and time of this AK should not be changed')) + event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), help_text=_('Associated event')) @@ -358,8 +363,10 @@ class AKSlot(models.Model): class AKOrgaMessage(models.Model): - ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'), help_text=_('AK this message belongs to')) - text = models.TextField(verbose_name=_("Message text"), help_text=_("Message to the organizers. This is not publicly visible.")) + ak = models.ForeignKey(to=AK, on_delete=models.CASCADE, verbose_name=_('AK'), + help_text=_('AK this message belongs to')) + text = models.TextField(verbose_name=_("Message text"), + help_text=_("Message to the organizers. This is not publicly visible.")) timestamp = models.DateTimeField(auto_now_add=True) class Meta: @@ -384,7 +391,8 @@ class ConstraintViolation(models.Model): REQUIRE_NOT_GIVEN = 'rng', _('Room does not satisfy the requirement of the scheduled AK') AK_CONFLICT_COLLISION = 'acc', _('AK Slot is scheduled at the same time as an AK listed as a conflict') AK_BEFORE_PREREQUISITE = 'abp', _('AK Slot is scheduled before an AK listed as a prerequisite') - AK_AFTER_RESODEADLINE = 'aar', _('AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline') + AK_AFTER_RESODEADLINE = 'aar', _( + 'AK Slot for AK with intention to submit a resolution is scheduled after resolution deadline') AK_CATEGORY_MISMATCH = 'acm', _('AK Slot in a category is outside that categories availabilities') AK_SLOT_COLLISION = 'asc', _('Two AK Slots for the same AK scheduled at the same time') ROOM_CAPACITY_EXCEEDED = 'rce', _('AK Slot is scheduled in a room with less space than interest') @@ -394,13 +402,18 @@ class ConstraintViolation(models.Model): WARNING = 1, _('Warning') VIOLATION = 10, _('Violation') - type = models.CharField(verbose_name=_('Type'), max_length=3, choices=ViolationType.choices, help_text=_('Type of violation, i.e. what kind of constraint was violated')) - level = models.PositiveSmallIntegerField(verbose_name=_('Level'), choices=ViolationLevel.choices, help_text=_('Severity level of the violation')) + type = models.CharField(verbose_name=_('Type'), max_length=3, choices=ViolationType.choices, + help_text=_('Type of violation, i.e. what kind of constraint was violated')) + level = models.PositiveSmallIntegerField(verbose_name=_('Level'), choices=ViolationLevel.choices, + help_text=_('Severity level of the violation')) - event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), help_text=_('Associated event')) + event = models.ForeignKey(to=Event, on_delete=models.CASCADE, verbose_name=_('Event'), + help_text=_('Associated event')) - aks = models.ManyToManyField(to=AK, blank=True, verbose_name=_('AKs'), help_text=_('AK(s) belonging to this constraint')) - ak_slots = models.ManyToManyField(to=AKSlot, blank=True, verbose_name=_('AK Slots'), help_text=_('AK Slot(s) belonging to this constraint')) + aks = models.ManyToManyField(to=AK, blank=True, verbose_name=_('AKs'), + help_text=_('AK(s) belonging to this constraint')) + ak_slots = models.ManyToManyField(to=AKSlot, blank=True, verbose_name=_('AK Slots'), + help_text=_('AK Slot(s) belonging to this constraint')) ak_owner = models.ForeignKey(to=AKOwner, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('AK Owner'), help_text=_('AK Owner belonging to this constraint')) room = models.ForeignKey(to=Room, on_delete=models.CASCADE, blank=True, null=True, verbose_name=_('Room'), @@ -437,6 +450,7 @@ class ConstraintViolation(models.Model): if a is not None: output.append(f"{field}: {a}") return ", ".join(output) + get_details.short_description = _('Details') def __str__(self): diff --git a/AKScheduling/api.py b/AKScheduling/api.py index 3601b55133b2cd17bba6dbea89f43236032ce567..687704a8e91a48ddd39383d391373d0d1a929648 100644 --- a/AKScheduling/api.py +++ b/AKScheduling/api.py @@ -48,10 +48,10 @@ class EventsView(LoginRequiredMixin, EventSlugMixin, ListView): "start": timezone.localtime(slot.start, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), "end": timezone.localtime(slot.end, self.event.timezone).strftime("%Y-%m-%d %H:%M:%S"), "backgroundColor": slot.ak.category.color, - # TODO Mark conflicts here? - "borderColor": slot.ak.category.color, + "borderColor": "#ff291d" if slot.fixed else slot.ak.category.color, "constraint": 'roomAvailable', - 'url': str(reverse('submit:ak_detail', kwargs={"event_slug": self.event.slug, "pk": slot.ak.pk})), + "editable": not slot.fixed, + 'url': str(reverse('admin:AKModel_akslot_change', args=[slot.pk])), } for slot in context["object_list"]], safe=False, **response_kwargs diff --git a/AKScheduling/templates/admin/AKScheduling/scheduling.html b/AKScheduling/templates/admin/AKScheduling/scheduling.html index 99b3ddde151305bb97314debf7f6aaf9a0a44681..eb8a01a8ff00614c326beb6f16ad332d26ece4b1 100644 --- a/AKScheduling/templates/admin/AKScheduling/scheduling.html +++ b/AKScheduling/templates/admin/AKScheduling/scheduling.html @@ -7,7 +7,7 @@ {% load static %} {% load tags_AKPlan %} -{% block title %}{% trans "Scheduling for" %} {{ event }}{% endblock %} +{% block title %}{% trans "Scheduling for" %} {{event}}{% endblock %} {% block extrahead %} {{ block.super }} @@ -94,12 +94,14 @@ buttonText: '{% trans "Day (Horizontal)" %}', slotDuration: '00:15', scrollTime: '08:00', + titleFormat: {weekday: 'long', day: 'numeric', month: 'numeric'}, }, resourceTimelineDayVert: { type: 'resourceTimeGridDay', buttonText: '{% trans "Day (Vertical)" %}', slotDuration: '00:30', scrollTime: '08:00', + titleFormat: {weekday: 'long', day: 'numeric', month: 'numeric'}, }, resourceTimelineEventHoriz: { type: 'resourceTimeline', @@ -174,6 +176,10 @@ } }); } + + $('.unscheduled-slot').each(function() { + $(this).tooltip({title: $(this).first().attr('data-details'), trigger: 'hover'}); + }); }); </script> {% endblock extrahead %} @@ -181,13 +187,13 @@ {% block content %} <div class="row" style="margin-bottom: 50px;"> - <div class="col-md-10 col-lg-11"> + <div class="col-md-10 col-lg-10"> <div id="planCalendar"></div> </div> - <div class="col-md-2 col-lg-1" id="unscheduled-slots"> + <div class="col-md-2 col-lg-2" id="unscheduled-slots"> {% for slot in slots_unscheduled %} <div class="unscheduled-slot badge badge-primary" style='background-color: {{ slot.ak.category.color }}' - data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "description": "{{ slot.ak.name }}", "slotID": "{{ slot.pk }}"}'>{{ slot.ak.short_name }} + data-event='{ "title": "{{ slot.ak.short_name }}", "duration": {"hours": "{{ slot.duration|unlocalize }}"}, "description": "{{ slot.ak.name }}", "slotID": "{{ slot.pk }}"}' data-details="{{ slot.ak.details }}">{{ slot.ak.short_name }} ({{ slot.duration }} h)<br>{{ slot.ak.owners_list }} </div> {% endfor %}