From 7a458e70b6c9d6df0c6c0567ca146d426f93a2a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?=
 <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de>
Date: Sun, 3 May 2020 20:26:04 +0200
Subject: [PATCH] Introduce color coding for recently changed akslots in plan

Add new helper property to model
Add and use template tag for color coding
Add helpfer functions for color blending (gradient and simple)
Also show timestamp of last edit in admin view
Introduce setting to control which changes will be higlighted
---
 AKModel/admin.py                           |  2 +
 AKModel/models.py                          | 11 +++-
 AKPlan/templates/AKPlan/encode_events.html |  5 +-
 AKPlan/templatetags/__init__.py            |  0
 AKPlan/templatetags/color_gradients.py     | 61 ++++++++++++++++++++++
 AKPlan/templatetags/tags_AKPlan.py         | 20 +++++++
 AKPlanning/settings.py                     |  2 +
 7 files changed, 98 insertions(+), 3 deletions(-)
 create mode 100644 AKPlan/templatetags/__init__.py
 create mode 100644 AKPlan/templatetags/color_gradients.py
 create mode 100644 AKPlan/templatetags/tags_AKPlan.py

diff --git a/AKModel/admin.py b/AKModel/admin.py
index 8d60bc7e..8446259d 100644
--- a/AKModel/admin.py
+++ b/AKModel/admin.py
@@ -74,6 +74,8 @@ admin.site.register(Room)
 
 @admin.register(AKSlot)
 class AKSlotAdmin(admin.ModelAdmin):
+    readonly_fields = ['updated']
+
     def get_form(self, request, obj=None, change=False, **kwargs):
         # Use timezone of associated event
         if obj is not None and obj.event.timezone:
diff --git a/AKModel/models.py b/AKModel/models.py
index 997e85e2..24ceb4d7 100644
--- a/AKModel/models.py
+++ b/AKModel/models.py
@@ -1,8 +1,8 @@
-# Create your models here.
 import datetime
 import itertools
 
 from django.db import models
+from django.utils import timezone
 from django.utils.text import slugify
 from django.utils.translation import gettext_lazy as _
 from timezone_field import TimeZoneField
@@ -301,3 +301,12 @@ class AKSlot(models.Model):
         Retrieve end time of the AK slot
         """
         return self.start + datetime.timedelta(hours=float(self.duration))
+
+    @property
+    def seconds_since_last_update(self):
+        """
+        Return minutes since last update
+        :return: minutes since last update
+        :rtype: float
+        """
+        return (timezone.now() - self.updated).total_seconds()
diff --git a/AKPlan/templates/AKPlan/encode_events.html b/AKPlan/templates/AKPlan/encode_events.html
index 2cb97b19..c38a4652 100644
--- a/AKPlan/templates/AKPlan/encode_events.html
+++ b/AKPlan/templates/AKPlan/encode_events.html
@@ -1,4 +1,5 @@
 {% load tz %}
+{% load tags_AKPlan %}
 
 [
     {% for slot in slots %}
@@ -8,8 +9,8 @@
                 'start': '{{ slot.start | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
                 'end': '{{ slot.end | timezone:event.timezone | date:"Y-m-d H:i:s" }}',
                 'resourceId': '{{ slot.room.title }}',
-                'backgroundColor': '{{ slot.ak.category.color }}',
-                'borderColor': '{{ slot.ak.track.color }}',
+                'backgroundColor': '{{ slot|highlight_change_colors }}',
+                'borderColor': '{{ slot.ak.category.color }}',
                 'url': '{% url 'submit:ak_detail' event_slug=event.slug pk=slot.ak.pk %}'
             },
         {% endif %}
diff --git a/AKPlan/templatetags/__init__.py b/AKPlan/templatetags/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/AKPlan/templatetags/color_gradients.py b/AKPlan/templatetags/color_gradients.py
new file mode 100644
index 00000000..1a9f96fd
--- /dev/null
+++ b/AKPlan/templatetags/color_gradients.py
@@ -0,0 +1,61 @@
+# gradients based on http://bsou.io/posts/color-gradients-with-python
+
+
+def hex_to_rgb(hex):
+    """
+    Convert hex color to RGB color code
+    :param hex: hex encoded color
+    :type hex: str
+    :return: rgb encoded version of given color
+    :rtype: list[int]
+    """
+    # Pass 16 to the integer function for change of base
+    return [int(hex[i:i+2], 16) for i in range(1,6,2)]
+
+
+def rgb_to_hex(rgb):
+    """
+    Convert rgb color (list) to hex encoding (str)
+    :param rgb: rgb encoded color
+    :type rgb: list[int]
+    :return: hex encoded version of given color
+    :rtype: str
+    """
+    # Components need to be integers for hex to make sense
+    rgb = [int(x) for x in rgb]
+    return "#"+"".join(["0{0:x}".format(v) if v < 16 else
+                        "{0:x}".format(v) for v in rgb])
+
+
+def linear_blend(start_hex, end_hex, position):
+    """
+    Create a linear blend between two colors and return color code on given position of the range from 0 to 1
+    :param start_hex: hex representation of start color
+    :type start_hex: str
+    :param end_hex: hex representation of end color
+    :type end_hex: str
+    :param position: position in range from 0 to 1
+    :type position: float
+    :return: hex encoded interpolated color
+    :rtype: str
+    """
+    s = hex_to_rgb(start_hex)
+    f = hex_to_rgb(end_hex)
+    blended = [int(s[j] + position * (f[j] - s[j])) for j in range(3)]
+    return rgb_to_hex(blended)
+
+
+def darken(start_hex, amount):
+    """
+    Darken the given color by the given amount (sensitivity will be cut in half)
+
+    :param start_hex: original color
+    :type start_hex: str
+    :param amount: how much to darken (1.0 -> 50% darker)
+    :type amount: float
+    :return: darker version of color
+    :rtype: str
+    """
+    start_rbg = hex_to_rgb(start_hex)
+    darker = [int(s * (1 - amount * .5)) for s in start_rbg]
+    return rgb_to_hex(darker)
diff --git a/AKPlan/templatetags/tags_AKPlan.py b/AKPlan/templatetags/tags_AKPlan.py
new file mode 100644
index 00000000..5618fd57
--- /dev/null
+++ b/AKPlan/templatetags/tags_AKPlan.py
@@ -0,0 +1,20 @@
+from django import template
+
+from AKPlan.templatetags.color_gradients import darken
+from AKPlanning import settings
+
+register = template.Library()
+
+
+@register.filter
+def highlight_change_colors(akslot):
+    seconds_since_update = akslot.seconds_since_last_update
+
+    # Last change long ago? Use default color
+    if seconds_since_update > settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS:
+        return akslot.ak.category.color
+
+    # Recent change? Calculate gradient blend between red and
+    recentness = seconds_since_update / settings.PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS
+    return darken("#b71540", recentness)
+    # return linear_blend("#b71540", "#000000", recentness)
diff --git a/AKPlanning/settings.py b/AKPlanning/settings.py
index fc67a542..32eb423d 100644
--- a/AKPlanning/settings.py
+++ b/AKPlanning/settings.py
@@ -164,5 +164,7 @@ PLAN_WALL_HOURS_RETROSPECT = 3
 PLAN_WALL_HOURS_FUTURE = 18
 # Should the plan use a hierarchy of buildings and rooms?
 PLAN_SHOW_HIERARCHY = True
+# For which time (in seconds) should changes of akslots be highlighted in plan?
+PLAN_MAX_HIGHLIGHT_UPDATE_SECONDS = 2 * 60 * 60
 
 include(optional("settings/*.py"))
-- 
GitLab