Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Features/history #107

Merged
merged 11 commits into from
Sep 17, 2023
2 changes: 1 addition & 1 deletion apps/showcase/templates/showcase/showcase.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "map.html" %}
{% extends "website/map.html" %}

{% load sass_tags %}
{% load static %}
Expand Down
Original file line number Diff line number Diff line change
@@ -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),
),
]
30 changes: 20 additions & 10 deletions apps/website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 12 additions & 4 deletions apps/website/scripts/add_demo_shares.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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


Expand Down
5 changes: 5 additions & 0 deletions apps/website/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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})"
Expand All @@ -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()
Expand Down
101 changes: 101 additions & 0 deletions apps/website/templates/website/history.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{% extends "website/map.html" %}

{% load sass_tags %}
{% load static %}

{% block main %}
<main id="history-page" class="w-100 flex-grow flex flex-row">
<section id="description"
class="w-20 tc mv1 mr1 pa2 flex flex-column justify-around items-center outline">

<section class="flex flex-column justify-center">
<section id="date-range" class="flex flex-row flex-wrap justify-center mv3">
{% for date in date_range %}
<a type="button" data-date="{{ date | date:"Y-m-d" }}"
onclick="selectDate(this)" href="javascript:void(0)"
class="link f5 b ma2 pa2 br-pill ba bw2 gray b--gray">
{{ date | date:"d M" }}
</a>
{% endfor %}
</section>

<section id="time-range" class="flex flex-row flex-wrap justify-center ma3">
{% for time in time_range %}
<a type="button" data-time="{{ time | time }}"
onclick="selectTime(this); update();" href="javascript:void(0)"
class="link f6 ma1 pa1 br-pill ba bw1 gray b--gray grow pointer">
{{ time | time }}
</a>
{% endfor %}
</section>
</section>

<section class="flex flex-row flex-wrap justify-around items-center ma3">
<section class="flex flex-column ma3">
<label for="date">Seleziona il giorno</label>
<input id="date" type="date"
value="{{ timestamp.current | date:"Y-m-d" }}"
min="{{ timestamp.first_date | date:"Y-m-d" }}"
max="{{ timestamp.last_date | date:"Y-m-d" }}">
</section>

<section class="flex flex-column ma3">
<label for="time">Seleziona l'orario</label>
<input id="time" type="time" class="tc"
value="{{ timestamp.current | time }}">
</section>

<section class="w-100 ma3 flex flex-center">
<a class="link grow pa2 ba bw2 b--black br4 pointer"
type="button" onclick="update()">
Aggiorna
</a>
</section>

</section>
</section>

<section id="full-map" class="w-80 mv1 ml1 pa2 outline">
<div id="map" style="height: 100%;"></div>
</section>
</main>
{% endblock %}

{% block scripts %}
{{ block.super }}

<script defer>
function update() {
const date = $("input#date").val();
const time = $("input#time").val();
window.location.href = `{% url "website:history" %}${date}T${time}:00`;
}

function selectDate(button) {
$("section#date-range > a.link")
.removeClass("near-black b--near-black")
.addClass("grow pointer gray b--gray");
$(button)
.removeClass('grow pointer gray b--gray')
.addClass("near-black b--near-black");
$("input#date").val($(button).attr("data-date"));
}

function selectTime(button) {
$("section#time-range > a.link")
.removeClass("b near-black b--near-black").addClass("gray b--gray");
$(button)
.removeClass('gray b--gray').addClass("b near-black b--near-black");
$("input#time").val($(button).attr("data-time"));
}

document.addEventListener('DOMContentLoaded', () => {
const current = {
date: "{{ timestamp.current | date:"Y-m-d" }}",
time: "{{ timestamp.current | time }}",
};
selectDate($(`section#date-range > a.link[data-date='${current.date}']`));
selectTime($(`section#time-range > a.link[data-time='${current.time}']`));
});
</script>
{% endblock %}
22 changes: 20 additions & 2 deletions apps/website/urls.py
Original file line number Diff line number Diff line change
@@ -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/<datetime:timestamp>/", views.HistoryMap.as_view(), name="history"),
]
60 changes: 58 additions & 2 deletions apps/website/views.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Loading