diff --git a/apps/showcase/templates/showcase/showcase.html b/apps/showcase/templates/showcase/showcase.html
index 3c3399f..7cb71b0 100644
--- a/apps/showcase/templates/showcase/showcase.html
+++ b/apps/showcase/templates/showcase/showcase.html
@@ -1,4 +1,4 @@
-{% extends "map.html" %}
+{% extends "website/map.html" %}
{% load sass_tags %}
{% load static %}
diff --git a/apps/website/migrations/0013_share_place_share_words_alter_share_timestamp.py b/apps/website/migrations/0013_share_place_share_words_alter_share_timestamp.py
new file mode 100644
index 0000000..54a3a27
--- /dev/null
+++ b/apps/website/migrations/0013_share_place_share_words_alter_share_timestamp.py
@@ -0,0 +1,37 @@
+# Generated by Django 4.2.5 on 2023-09-16 19:06
+
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+ dependencies = [
+ ("website", "0012_landscape_enabled_alter_landscape_domain"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="share",
+ name="place",
+ field=models.ForeignKey(
+ blank=True,
+ null=True,
+ on_delete=django.db.models.deletion.SET_NULL,
+ related_name="shares",
+ to="website.place",
+ ),
+ ),
+ migrations.AddField(
+ model_name="share",
+ name="words",
+ field=models.ManyToManyField(
+ blank=True, related_name="shares", to="website.word"
+ ),
+ ),
+ migrations.AlterField(
+ model_name="share",
+ name="timestamp",
+ field=models.DateTimeField(default=django.utils.timezone.now),
+ ),
+ ]
diff --git a/apps/website/models.py b/apps/website/models.py
index 7421c5c..f22fc84 100644
--- a/apps/website/models.py
+++ b/apps/website/models.py
@@ -7,6 +7,7 @@
from django.contrib.gis.geos import Point
from django.db.models import F, Max, Q, Sum
from django.shortcuts import get_object_or_404
+from django.utils import timezone
from django.utils.translation import gettext as _
from .fields import UniqueBooleanField
@@ -50,16 +51,6 @@ class Meta:
abstract = True
-class Share(LocationModel):
- timestamp = models.DateTimeField(auto_now_add=True)
- message = models.TextField(max_length=500)
- landscape = models.ForeignKey("Landscape", on_delete=models.CASCADE)
-
- def __str__(self):
- message = textwrap.shorten(self.message, width=20, placeholder="...")
- return f"{super().__str__()} [{message}]"
-
-
class LeafletProvider(TitledModel):
name = models.CharField(
max_length=100,
@@ -133,6 +124,25 @@ def __str__(self):
return ("🚩 " if not self.visible else "") + self.text
+class Share(LocationModel):
+ timestamp = models.DateTimeField(default=timezone.now)
+ message = models.TextField(max_length=500)
+ landscape = models.ForeignKey("Landscape", on_delete=models.CASCADE)
+
+ place = models.ForeignKey(
+ Place,
+ related_name="shares",
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ )
+ words = models.ManyToManyField(Word, related_name="shares", blank=True)
+
+ def __str__(self):
+ message = textwrap.shorten(self.message, width=20, placeholder="...")
+ return f"{super().__str__()} [{message}]"
+
+
class WordFrequency(models.Model):
word = models.ForeignKey(
Word,
diff --git a/apps/website/scripts/add_demo_shares.py b/apps/website/scripts/add_demo_shares.py
index 485f2ab..8f4930b 100644
--- a/apps/website/scripts/add_demo_shares.py
+++ b/apps/website/scripts/add_demo_shares.py
@@ -11,7 +11,7 @@
from django.conf import settings
from django.contrib.gis.geos import Point
-from loguru import logger
+from django.utils import timezone
from tqdm import tqdm
from .. import models
@@ -21,12 +21,20 @@
TEXT_ROW_RE = re.compile(r"[A-Za-z]")
+def random_timestamp(delta=1440):
+ minutes = round(random.gauss(0, delta))
+ return timezone.now() + timezone.timedelta(minutes=minutes)
+
+
def save_share(location: Point, message: str, landscape: models.Landscape):
- defaults = {"location": location, "message": message, "landscape": landscape}
- share = models.Share(**defaults)
+ share = models.Share(
+ location=location,
+ message=message,
+ landscape=landscape,
+ timestamp=random_timestamp(),
+ )
share.full_clean()
share.save()
- logger.debug(f"Created Share: {share}")
return share
diff --git a/apps/website/signals.py b/apps/website/signals.py
index b582435..82dde5e 100644
--- a/apps/website/signals.py
+++ b/apps/website/signals.py
@@ -38,6 +38,9 @@ def on_share_creation_update_frequencies(
.first()
)
+ instance.place = place
+ instance.save()
+
logger.debug(
f"Receive share near [{place}], update WordFrequency "
f"(message: {instance.message})"
@@ -59,6 +62,8 @@ def on_share_creation_update_frequencies(
word.full_clean()
word.save()
+ instance.words.add(word)
+
wf, _ = models.WordFrequency.objects.get_or_create(place=place, word=word)
wf.frequency = F("frequency") + 1
wf.save()
diff --git a/apps/website/templates/website/history.html b/apps/website/templates/website/history.html
new file mode 100644
index 0000000..64fbf11
--- /dev/null
+++ b/apps/website/templates/website/history.html
@@ -0,0 +1,101 @@
+{% extends "website/map.html" %}
+
+{% load sass_tags %}
+{% load static %}
+
+{% block main %}
+
+
+
+
+
+{% endblock %}
+
+{% block scripts %}
+ {{ block.super }}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/apps/website/templates/map.html b/apps/website/templates/website/map.html
similarity index 100%
rename from apps/website/templates/map.html
rename to apps/website/templates/website/map.html
diff --git a/apps/website/templates/share.html b/apps/website/templates/website/share.html
similarity index 100%
rename from apps/website/templates/share.html
rename to apps/website/templates/website/share.html
diff --git a/apps/website/urls.py b/apps/website/urls.py
index 401d5e8..ca32556 100644
--- a/apps/website/urls.py
+++ b/apps/website/urls.py
@@ -1,11 +1,29 @@
-from django.urls import path
-from django.views.generic import RedirectView
+from django.urls import path, register_converter
+from django.utils import timezone
from . import views
+
+class DateTimeConverter:
+ regex = r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"
+
+ @staticmethod
+ def to_python(value):
+ time = timezone.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S")
+ return timezone.make_aware(time)
+
+ @staticmethod
+ def to_url(value):
+ return value
+
+
+register_converter(DateTimeConverter, "datetime")
+
app_name = "website"
urlpatterns = [
path("", views.Map.as_view()),
path("map/", views.Map.as_view(), name="map"),
path("share/", views.Share.as_view(), name="share"),
+ path("history/", views.HistoryMap.as_view(), name="history"),
+ path("history//", views.HistoryMap.as_view(), name="history"),
]
diff --git a/apps/website/views.py b/apps/website/views.py
index 8e49372..8c7b1ad 100644
--- a/apps/website/views.py
+++ b/apps/website/views.py
@@ -1,7 +1,13 @@
+import datetime as dt
+from collections import defaultdict
+from typing import Iterable
+
from django.contrib import messages
from django.contrib.gis.geos import Point
from django.core.exceptions import ObjectDoesNotExist
+from django.db.models import Max, Min
from django.shortcuts import get_object_or_404, redirect
+from django.utils import timezone
from django.utils.translation import gettext as _
from django.views.generic import TemplateView
@@ -50,7 +56,7 @@ def get_context_data(self, **kwargs):
class Share(LandscapeTemplateView):
- template_name = "share.html"
+ template_name = "website/share.html"
def post(self, request):
form = forms.ShareForm(request.POST)
@@ -82,4 +88,54 @@ def get_context_data(self, **kwargs):
class Map(MapTemplateView):
- template_name = "map.html"
+ template_name = "website/map.html"
+
+
+class HistoryMap(MapTemplateView):
+ template_name = "website/history.html"
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+
+ timestamp = kwargs.get("timestamp", timezone.now())
+ counters = {
+ place: defaultdict(lambda: 0) for place in models.Place.objects.all()
+ }
+
+ for share in models.Share.objects.filter(timestamp__lt=timestamp):
+ for word in share.words.all():
+ counters[share.place][word] += 1
+
+ output = []
+ for place, counter in counters.items():
+ if not counter:
+ continue
+ max_value = max(counter.values())
+ frequencies = [
+ [word.text, count / max_value] for word, count in counter.items()
+ ]
+ output.append(
+ {"coordinates": place.coordinates, "frequencies": frequencies}
+ )
+
+ context["places"] = output
+
+ timestamp_range = models.Share.objects.aggregate(
+ min=Min("timestamp"), max=Max("timestamp")
+ )
+ context["timestamp"] = {
+ "current": timestamp,
+ "first_date": (d_min := timestamp_range["min"].date()),
+ "last_date": (d_max := timestamp_range["max"].date()),
+ }
+ context["date_range"] = list(date_range(d_min, d_max))
+ context["time_range"] = [dt.time(h, 0) for h in range(24)]
+
+ return context
+
+
+def date_range(first_date: dt.date, last_date: dt.date) -> Iterable[dt.date]:
+ date = first_date
+ while date <= last_date:
+ yield date
+ date += timezone.timedelta(days=1)
diff --git a/makefile b/makefile
index 80d2e7a..9ea68c0 100644
--- a/makefile
+++ b/makefile
@@ -75,12 +75,7 @@ collectstatic:
# Django database commands
-.PHONY: bootstrap-django clean-django demo migrate migrations secret_key superuser sqlite-bootstrap
-
-bootstrap-django: clean-django secret_key sqlite-bootstrap migrate superuser
-
-clean-django:
- @rm -rf db.sqlite3 .media .static
+.PHONY: demo migrate migrations secret_key superuser
demo:
@LOGURU_LEVEL=INFO $(django) runscript init_demo
@@ -100,13 +95,12 @@ superuser:
@echo -e $(bold)Creating superuser account 'admin'$(sgr0)
@$(django) createsuperuser --username=admin --email=voci@afor.dev
-sqlite-bootstrap:
- @echo -e $(bold)Prepare SQLite db with GeoDjango enabled$(sgr0)
- # Temporary solution for https://code.djangoproject.com/ticket/32935
- @$(django) shell -c "import django;django.db.connection.cursor().execute('SELECT InitSpatialMetaData(1);')";
+# Database related commands
+.PHONY: bootstrap-sqlite db-flush db-demo pg-dump sqlite-reset
+
+bootstrap-sqlite: sqlite-reset migrate superuser
-.PHONY: db-flush db-demo pg-dump
db-flush:
@echo -e $(bold)Deleting all data from database$(sgr0)
@$(django) flush
@@ -122,4 +116,10 @@ PG_NAME?=landscapes
pg-dump:
@mkdir -p .backup/
- @pg_dump -U $(PG_USER) $(PG_NAME) | gzip -9 > .backup/landscapes."$(shell date --iso-8601=seconds)".sql.gz
\ No newline at end of file
+ @pg_dump -U $(PG_USER) $(PG_NAME) | gzip -9 > .backup/landscapes."$(shell date --iso-8601=seconds)".sql.gz
+
+sqlite-reset:
+ @echo -e $(bold)Prepare SQLite db with GeoDjango enabled$(sgr0)
+ @rm -rf db.sqlite3 .media .static
+ # Temporary solution for https://code.djangoproject.com/ticket/32935
+ @$(django) shell -c "import django;django.db.connection.cursor().execute('SELECT InitSpatialMetaData(1);')";