From 9fbfe583beac379518d52172cb868dd5acf2ca9f Mon Sep 17 00:00:00 2001 From: Kumi Date: Fri, 17 May 2024 15:03:20 +0200 Subject: [PATCH] feat: Enhance data security and introduce heatmap UI Reinforced user data access rules to bolster security and reorganized distribution files into separate directories for cleaner structure. Added a new heatmap visualization for mood statistics on the dashboard, making user engagements more interactive and insightful. Implemented a JSON view to support the heatmap feature, fetching mood entries within a specified time range. This change responds to the need for improved data security and a more engaging user interface, directly addressing user feedback for clearer insights into their mood patterns over time. --- TODO.md | 4 +- dreams/templatetags/dream_stats.py | 49 +-- .../frontend/{ => dist}/css/cal-heatmap.css | 0 .../frontend/{ => dist}/js/cal-heatmap.min.js | 0 .../frontend/{ => dist}/js/d3.v7.min.js | 0 frontend/static/frontend/js/dashboard.js | 27 ++ frontend/templates/frontend/dashboard.html | 355 ++++++++++-------- frontend/views.py | 12 +- mood/statistics.py | 3 +- mood/urls.py | 101 +++-- mood/views.py | 183 +++++++-- 11 files changed, 507 insertions(+), 227 deletions(-) rename frontend/static/frontend/{ => dist}/css/cal-heatmap.css (100%) rename frontend/static/frontend/{ => dist}/js/cal-heatmap.min.js (100%) rename frontend/static/frontend/{ => dist}/js/d3.v7.min.js (100%) create mode 100644 frontend/static/frontend/js/dashboard.js diff --git a/TODO.md b/TODO.md index ec7690d..5359ebc 100644 --- a/TODO.md +++ b/TODO.md @@ -2,10 +2,12 @@ ## General -- [_] Make sure users can't access other users' data +- [_] Make extra sure users can't access other users' data - [_] User profiles / names / gateway config / time zone settings - [_] Implement a proper logging system - [_] Implement a proper API +- [_] Move dist files to separate directories +- [_] Turn this into issues ## cbt module diff --git a/dreams/templatetags/dream_stats.py b/dreams/templatetags/dream_stats.py index 2719aee..537b861 100644 --- a/dreams/templatetags/dream_stats.py +++ b/dreams/templatetags/dream_stats.py @@ -1,24 +1,32 @@ from django import template -from django.utils import timezone +from datetime import datetime, timedelta from collections import Counter register = template.Library() + @register.simple_tag(takes_context=True) def total_dreams(context): return len(context["user"].dream_set.all()) + @register.simple_tag(takes_context=True) def weekly_dreams(context): - now = timezone.now() - start = now - timezone.timedelta(days=7) + now = datetime.now() + start = now - timedelta(days=7) + + return len( + context["user"].dream_set.filter(timestamp__gte=start, timestamp__lte=now) + ) - return len(context["user"].dream_set.filter(timestamp__gte=start, timestamp__lte=now)) @register.simple_tag(takes_context=True) def most_common_theme(context, start, end=None): - dream_list = context["user"].dream_set.filter(timestamp__gte=start.date(), timestamp__lte=(end.date() if end else start.date())) + dream_list = context["user"].dream_set.filter( + timestamp__gte=start.date(), + timestamp__lte=(end.date() if end else start.date()), + ) themes = list() for dream in dream_list: @@ -28,33 +36,32 @@ def most_common_theme(context, start, end=None): try: most_common = Counter(themes).most_common(1)[0] return most_common[0], most_common[1] - except: + except Exception: return None, None + @register.simple_tag(takes_context=True) def most_common_theme_weekly(context): - now = timezone.now() - start = now - timezone.timedelta(days=7) + now = datetime.now() + start = now - timedelta(days=7) return most_common_theme(context, start, now) + @register.simple_tag(takes_context=True) def special_dreams(context, start, end=None): - dream_list = context["user"].dream_set.filter(timestamp__gte=start.date(), timestamp__lte=(end.date() if end else start.date())) - wet = 0 - lucid = 0 - - for dream in dream_list: - if dream.lucid: - lucid += 1 - if dream.wet: - wet += 1 - + dream_list = context["user"].dream_set.filter( + timestamp__gte=start.date(), + timestamp__lte=(end.date() if end else start.date()), + ) + lucid = sum([1 for dream in dream_list if dream.lucid]) + wet = sum([1 for dream in dream_list if dream.wet]) return lucid, wet + @register.simple_tag(takes_context=True) def special_dreams_weekly(context): - now = timezone.now() - start = now - timezone.timedelta(days=7) + now = datetime.now() + start = now - timedelta(days=7) - return special_dreams(context, start, now) \ No newline at end of file + return special_dreams(context, start, now) diff --git a/frontend/static/frontend/css/cal-heatmap.css b/frontend/static/frontend/dist/css/cal-heatmap.css similarity index 100% rename from frontend/static/frontend/css/cal-heatmap.css rename to frontend/static/frontend/dist/css/cal-heatmap.css diff --git a/frontend/static/frontend/js/cal-heatmap.min.js b/frontend/static/frontend/dist/js/cal-heatmap.min.js similarity index 100% rename from frontend/static/frontend/js/cal-heatmap.min.js rename to frontend/static/frontend/dist/js/cal-heatmap.min.js diff --git a/frontend/static/frontend/js/d3.v7.min.js b/frontend/static/frontend/dist/js/d3.v7.min.js similarity index 100% rename from frontend/static/frontend/js/d3.v7.min.js rename to frontend/static/frontend/dist/js/d3.v7.min.js diff --git a/frontend/static/frontend/js/dashboard.js b/frontend/static/frontend/js/dashboard.js new file mode 100644 index 0000000..2017b6c --- /dev/null +++ b/frontend/static/frontend/js/dashboard.js @@ -0,0 +1,27 @@ +const cal = new CalHeatmap(); +cal.paint({ + itemSelector: "#mood-count-heatmap", + data: { + source: + "/mood/statistics/heatmap/?start={{start=YYYY-MM-DD}}&end={{end=YYYY-MM-DD}}", + x: "date", + y: "value", + }, + date: { + start: new Date(new Date().setFullYear(new Date().getFullYear() - 1)), // Start from one year ago + }, + range: 13, // Display 13 months so that the current month is included + domain: { + type: "month", + label: { + position: "top", + text: "MMM YYYY", + }, + }, + subDomain: { + type: "ghDay", + width: 10, + height: 10, + }, + highlight: [new Date()], +}); diff --git a/frontend/templates/frontend/dashboard.html b/frontend/templates/frontend/dashboard.html index e807c8b..9fe8ae3 100644 --- a/frontend/templates/frontend/dashboard.html +++ b/frontend/templates/frontend/dashboard.html @@ -1,174 +1,221 @@ -{% extends "frontend/base.html" %} -{% load mood_stats %} -{% load dream_stats %} +{% extends "frontend/base.html" %} {% load mood_stats %} {% load dream_stats %} {% block "content" %} +

Moods

+ +
- - -
-
-
-
-
-
- Status count (total)
-
{% total_moods %}
-
-
- -
-
-
-
-
- - -
-
-
-
-
-
- Current Streak Length
-
{% current_streak %} days
-
-
- -
-
-
-
-
- - -
-
-
-
-
-
Average mood (weekly) -
-
-
- {% average_mood_weekly as mood %} - {% closest_mood mood as moodobj %} -
{{ moodobj }} ({{ mood|floatformat:2 }})
-
-
-
-
- - -
-
-
-
-
- - -
-
-
-
-
-
- Most Common Activity (weekly)
- {% most_common_activity_weekly as activity %} -
{{ activity.0 }} ({{ activity.1 }})
-
-
- -
-
-
-
+
+
+
+
Mood Calendar
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+ Status count (total) +
+
+ {% total_moods %} +
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ Current Streak Length +
+
+ {% current_streak %} days +
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ Average mood (weekly) +
+
+
+ {% average_mood_weekly as mood %} {% closest_mood mood as moodobj %} +
+ {{ moodobj }} ({{ mood|floatformat:2 }}) +
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ Most Common Activity (weekly) +
+ {% most_common_activity_weekly as activity %} +
+ {{ activity.0 }} ({{ activity.1 }}) +
+
+
+ +
+
+
+
+
+
+ +

Dreams

+
- - -
-
-
-
-
-
- Dream count (total)
-
{% total_dreams %}
-
-
- -
-
+ +
+
+
+
+
+
+ Dream count (total)
-
-
- - -
-
-
-
-
-
- Dreams (weekly)
-
{% weekly_dreams %}
-
-
- -
-
+
+ {% total_dreams %}
+
+
+ +
+
+
- -
-
-
-
-
-
Special Dreams (lucid / wet) -
-
-
- {% special_dreams_weekly as weekly %} -
{{ weekly.0 }} / {{ weekly.1 }} (weekly)
-
-
-
-
- - -
-
+ +
+
+
+
+
+
+ Dreams (weekly)
-
-
- - -
-
-
-
-
-
- Most Common Theme (weekly)
- {% most_common_theme_weekly as theme %} -
{{ theme.0 }} ({{ theme.1 }})
-
-
- -
-
+
+ {% weekly_dreams %}
+
+
+ +
+
+
+ + +
+
+
+
+
+
+ Special Dreams (lucid / wet) +
+
+
+ {% special_dreams_weekly as weekly %} +
+ {{ weekly.0 }} / {{ weekly.1 }} (weekly) +
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+
+
+ Most Common Theme (weekly) +
+ {% most_common_theme_weekly as theme %} +
+ {{ theme.0 }} ({{ theme.1 }}) +
+
+
+ +
+
+
+
+
- -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/frontend/views.py b/frontend/views.py index be80813..7b26b08 100644 --- a/frontend/views.py +++ b/frontend/views.py @@ -11,7 +11,15 @@ class DashboardView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["title"] = "Dashboard" - context["subtitle"] = "An overview of everything going on in your Kumify account." + context["subtitle"] = ( + "An overview of everything going on in your Kumify account." + ) + context["scripts"] = [ + "frontend/dist/js/d3.v7.min.js", + "frontend/dist/js/cal-heatmap.min.js", + "frontend/js/dashboard.js", + ] + context["styles"] = ["frontend/dist/css/cal-heatmap.css"] return context @@ -26,4 +34,4 @@ class UserRegistrationView(CreateView): return ret def get_success_url(self): - return reverse_lazy("frontend:dashboard") \ No newline at end of file + return reverse_lazy("frontend:dashboard") diff --git a/mood/statistics.py b/mood/statistics.py index a99158c..2d3c154 100644 --- a/mood/statistics.py +++ b/mood/statistics.py @@ -6,10 +6,9 @@ from django.utils import timezone from math import pi from bokeh.models import HoverTool -from bokeh.io import output_file, show from bokeh.plotting import figure from bokeh.transform import cumsum -from bokeh.layouts import row, column +from bokeh.layouts import column from holoviews.operation import timeseries from dateutil.relativedelta import relativedelta diff --git a/mood/urls.py b/mood/urls.py index 50f665e..d3be7bb 100644 --- a/mood/urls.py +++ b/mood/urls.py @@ -1,30 +1,83 @@ -from .views import StatusListView, StatusViewView, StatusDeleteView, StatusEditView, StatusCreateView, ActivityListView, ActivityEditView, ActivityCreateView, ActivityDeleteView, MoodListView, MoodEditView, NotificationCreateView, NotificationDeleteView, NotificationEditView, NotificationListView, MoodStatisticsView, MoodCSVView, MoodPlotView, MoodPiesView, ActivityStatisticsView, ActivityPlotView, ActivityPiesView +from .views import ( + StatusListView, + StatusViewView, + StatusDeleteView, + StatusEditView, + StatusCreateView, + ActivityListView, + ActivityEditView, + ActivityCreateView, + ActivityDeleteView, + MoodListView, + MoodEditView, + NotificationCreateView, + NotificationDeleteView, + NotificationEditView, + NotificationListView, + MoodStatisticsView, + MoodCSVView, + MoodPlotView, + MoodPiesView, + ActivityStatisticsView, + ActivityPlotView, + ActivityPiesView, + MoodCountHeatmapJSONView, +) from django.urls import path, include app_name = "mood" urlpatterns = [ - path('', StatusListView.as_view(), name="status_list"), - path('status//view/', StatusViewView.as_view(), name="status_view"), - path('status//edit/', StatusEditView.as_view(), name="status_edit"), - path('status//delete/', StatusDeleteView.as_view(), name="status_delete"), - path('status/new/', StatusCreateView.as_view(), name="status_create"), - path('activity/', ActivityListView.as_view(), name="activity_list"), - path('activity//edit/', ActivityEditView.as_view(), name="activity_edit"), - path('activity/new/', ActivityCreateView.as_view(), name="activity_create"), - path('activity//delete/', ActivityDeleteView.as_view(), name="activity_delete"), - path('mood/', MoodListView.as_view(), name="mood_list"), - path('mood//edit/', MoodEditView.as_view(), name="mood_edit"), - path('notification/', NotificationListView.as_view(), name="notification_list"), - path('notification//edit/', NotificationEditView.as_view(), name="notification_edit"), - path('notification//delete/', NotificationDeleteView.as_view(), name="notification_delete"), - path('notification/new/', NotificationCreateView.as_view(), name="notification_create"), - path('statistics/', MoodStatisticsView.as_view(), name="statistics"), - path('statistics/csv/', MoodCSVView.as_view(), name="statistics_csv"), - path('statistics/plot/', MoodPlotView.as_view(), name="statistics_plot"), - path('statistics/pies/', MoodPiesView.as_view(), name="statistics_pies"), - path('statistics/activity//', ActivityStatisticsView.as_view(), name="statistics_activity"), - path('statistics/activity//plot/', ActivityPlotView.as_view(), name="statistics_activity_plot"), - path('statistics/activity//pies/', ActivityPiesView.as_view(), name="statistics_activity_pies"), -] \ No newline at end of file + path("", StatusListView.as_view(), name="status_list"), + path("status//view/", StatusViewView.as_view(), name="status_view"), + path("status//edit/", StatusEditView.as_view(), name="status_edit"), + path("status//delete/", StatusDeleteView.as_view(), name="status_delete"), + path("status/new/", StatusCreateView.as_view(), name="status_create"), + path("activity/", ActivityListView.as_view(), name="activity_list"), + path("activity//edit/", ActivityEditView.as_view(), name="activity_edit"), + path("activity/new/", ActivityCreateView.as_view(), name="activity_create"), + path( + "activity//delete/", + ActivityDeleteView.as_view(), + name="activity_delete", + ), + path("mood/", MoodListView.as_view(), name="mood_list"), + path("mood//edit/", MoodEditView.as_view(), name="mood_edit"), + path("notification/", NotificationListView.as_view(), name="notification_list"), + path( + "notification//edit/", + NotificationEditView.as_view(), + name="notification_edit", + ), + path( + "notification//delete/", + NotificationDeleteView.as_view(), + name="notification_delete", + ), + path( + "notification/new/", + NotificationCreateView.as_view(), + name="notification_create", + ), + path("statistics/", MoodStatisticsView.as_view(), name="statistics"), + path("statistics/csv/", MoodCSVView.as_view(), name="statistics_csv"), + path("statistics/heatmap/", MoodCountHeatmapJSONView.as_view(), name="statistics_heatmap"), + path("statistics/plot/", MoodPlotView.as_view(), name="statistics_plot"), + path("statistics/pies/", MoodPiesView.as_view(), name="statistics_pies"), + path( + "statistics/activity//", + ActivityStatisticsView.as_view(), + name="statistics_activity", + ), + path( + "statistics/activity//plot/", + ActivityPlotView.as_view(), + name="statistics_activity_plot", + ), + path( + "statistics/activity//pies/", + ActivityPiesView.as_view(), + name="statistics_activity_pies", + ), +] diff --git a/mood/views.py b/mood/views.py index 466fca6..6a44979 100644 --- a/mood/views.py +++ b/mood/views.py @@ -1,4 +1,12 @@ -from django.views.generic import TemplateView, ListView, UpdateView, DetailView, CreateView, DeleteView, View +from django.views.generic import ( + TemplateView, + ListView, + UpdateView, + DetailView, + CreateView, + DeleteView, + View, +) from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404 from django.urls import reverse_lazy @@ -6,6 +14,7 @@ from django.http import HttpResponseRedirect, HttpResponse from django.utils import timezone from django.views.decorators.clickjacking import xframe_options_sameorigin from django.utils.decorators import method_decorator +from django.db.models import Count from .models import Status, Activity, Mood, StatusMedia, StatusActivity from .forms import StatusForm @@ -19,6 +28,9 @@ from dateutil import relativedelta from datetime import datetime +import json + + class StatusListView(LoginRequiredMixin, ListView): template_name = "mood/status_list.html" model = Status @@ -27,11 +39,13 @@ class StatusListView(LoginRequiredMixin, ListView): context = super().get_context_data(**kwargs) context["title"] = "Status List" context["subtitle"] = "Just a list of your mood entries." - context["buttons"] = [(reverse_lazy("mood:status_create"), "New Status", "plus")] + context["buttons"] = [ + (reverse_lazy("mood:status_create"), "New Status", "plus") + ] return context def get_queryset(self): - return Status.objects.filter(user=self.request.user).order_by('timestamp') + return Status.objects.filter(user=self.request.user).order_by("timestamp") class StatusViewView(LoginRequiredMixin, DetailView): @@ -42,7 +56,13 @@ class StatusViewView(LoginRequiredMixin, DetailView): context = super().get_context_data(**kwargs) context["title"] = "View Status" context["subtitle"] = "View the details of your mood entry." - context["buttons"] = [(reverse_lazy("mood:status_edit", kwargs={"id": self.kwargs["id"]}), "Edit Status", "pen")] + context["buttons"] = [ + ( + reverse_lazy("mood:status_edit", kwargs={"id": self.kwargs["id"]}), + "Edit Status", + "pen", + ) + ] return context def get_object(self): @@ -91,7 +111,13 @@ class StatusEditView(LoginRequiredMixin, UpdateView): context["title"] = "Update Status" context["subtitle"] = "Change a status you created before." context["scripts"] = ["frontend/js/dropdown-to-buttons.js"] - context["buttons"] = [(reverse_lazy("mood:status_delete", kwargs={"id": self.kwargs["id"]}), "Delete Status", "trash-alt")] + context["buttons"] = [ + ( + reverse_lazy("mood:status_delete", kwargs={"id": self.kwargs["id"]}), + "Delete Status", + "trash-alt", + ) + ] return context def get_object(self): @@ -102,11 +128,13 @@ class StatusEditView(LoginRequiredMixin, UpdateView): dba = StatusMedia(status=form.instance) dba.file.save(get_upload_path(form.instance, attachment.name), attachment) dba.save() - + for activity in form.cleaned_data["activities"]: if activity.user == self.request.user: if not activity in form.instance.activity_set: - StatusActivity.objects.create(activity=activity, status=form.instance) + StatusActivity.objects.create( + activity=activity, status=form.instance + ) for statusactivity in form.instance.statusactivity_set.all(): if not statusactivity.activity in form.cleaned_data["activities"]: @@ -137,7 +165,9 @@ class ActivityListView(LoginRequiredMixin, ListView): context = super().get_context_data(**kwargs) context["title"] = "Activities" context["subtitle"] = "The activities you have defined." - context["buttons"] = [(reverse_lazy("mood:activity_create"), "Create Activity", "pen")] + context["buttons"] = [ + (reverse_lazy("mood:activity_create"), "Create Activity", "pen") + ] return context def get_queryset(self): @@ -153,9 +183,20 @@ class ActivityEditView(LoginRequiredMixin, UpdateView): context = super().get_context_data(**kwargs) context["title"] = "Edit Activity" context["subtitle"] = "Make changes to the activity." - context["scripts"] = ["colorfield/jscolor/jscolor.js", "colorfield/colorfield.js", "frontend/js/fontawesome-iconpicker.min.js", "frontend/js/iconpicker-loader.js"] + context["scripts"] = [ + "colorfield/jscolor/jscolor.js", + "colorfield/colorfield.js", + "frontend/js/fontawesome-iconpicker.min.js", + "frontend/js/iconpicker-loader.js", + ] context["styles"] = ["frontend/css/fontawesome-iconpicker.min.css"] - context["buttons"] = [(reverse_lazy("mood:activity_delete", kwargs={"id": self.kwargs["id"]}), "Delete Activity", "trash-alt")] + context["buttons"] = [ + ( + reverse_lazy("mood:activity_delete", kwargs={"id": self.kwargs["id"]}), + "Delete Activity", + "trash-alt", + ) + ] return context def get_object(self): @@ -174,7 +215,12 @@ class ActivityCreateView(LoginRequiredMixin, CreateView): context = super().get_context_data(**kwargs) context["title"] = "Create Activity" context["subtitle"] = "Add a new activity." - context["scripts"] = ["colorfield/jscolor/jscolor.js", "colorfield/colorfield.js", "frontend/js/fontawesome-iconpicker.min.js", "frontend/js/iconpicker-loader.js"] + context["scripts"] = [ + "colorfield/jscolor/jscolor.js", + "colorfield/colorfield.js", + "frontend/js/fontawesome-iconpicker.min.js", + "frontend/js/iconpicker-loader.js", + ] context["styles"] = ["frontend/css/fontawesome-iconpicker.min.css"] return context @@ -221,7 +267,12 @@ class MoodEditView(LoginRequiredMixin, UpdateView): context = super().get_context_data(**kwargs) context["title"] = "Edit Mood" context["subtitle"] = "Make changes to the mood." - context["scripts"] = ["colorfield/jscolor/jscolor.js", "colorfield/colorfield.js", "frontend/js/fontawesome-iconpicker.min.js", "frontend/js/iconpicker-loader.js"] + context["scripts"] = [ + "colorfield/jscolor/jscolor.js", + "colorfield/colorfield.js", + "frontend/js/fontawesome-iconpicker.min.js", + "frontend/js/iconpicker-loader.js", + ] context["styles"] = ["frontend/css/fontawesome-iconpicker.min.css"] return context @@ -241,7 +292,12 @@ class MoodCreateView(LoginRequiredMixin, CreateView): context = super().get_context_data(**kwargs) context["title"] = "Create Activity" context["subtitle"] = "Add a new activity." - context["scripts"] = ["colorfield/jscolor/jscolor.js", "colorfield/colorfield.js", "frontend/js/fontawesome-iconpicker.min.js", "frontend/js/iconpicker-loader.js"] + context["scripts"] = [ + "colorfield/jscolor/jscolor.js", + "colorfield/colorfield.js", + "frontend/js/fontawesome-iconpicker.min.js", + "frontend/js/iconpicker-loader.js", + ] context["styles"] = ["frontend/css/fontawesome-iconpicker.min.css"] return context @@ -263,11 +319,15 @@ class NotificationListView(LoginRequiredMixin, ListView): context = super().get_context_data(**kwargs) context["title"] = "Notifications" context["subtitle"] = "The daily reminders you have set up." - context["buttons"] = [(reverse_lazy("mood:notification_create"), "New Notification", "plus")] + context["buttons"] = [ + (reverse_lazy("mood:notification_create"), "New Notification", "plus") + ] return context def get_queryset(self): - return NotificationDailySchedule.objects.filter(notification__recipient=self.request.user, notification__app="mood") + return NotificationDailySchedule.objects.filter( + notification__recipient=self.request.user, notification__app="mood" + ) class NotificationCreateView(LoginRequiredMixin, CreateView): @@ -282,7 +342,11 @@ class NotificationCreateView(LoginRequiredMixin, CreateView): return context def form_valid(self, form): - notification = Notification.objects.create(content="Hi, it's time for a new Kumify entry! Go to %KUMIFYURL% to document your mood!", recipient=self.request.user, app="mood") + notification = Notification.objects.create( + content="Hi, it's time for a new Kumify entry! Go to %KUMIFYURL% to document your mood!", + recipient=self.request.user, + app="mood", + ) obj = form.save(commit=False) obj.notification = notification return super().form_valid(form) @@ -300,14 +364,23 @@ class NotificationEditView(LoginRequiredMixin, UpdateView): context = super().get_context_data(**kwargs) context["title"] = "Edit Notification" context["subtitle"] = "Change the time of a daily notification." - context["buttons"] = [(reverse_lazy("mood:notification_delete", args=[self.kwargs["id"]]), "Delete Notification")] + context["buttons"] = [ + ( + reverse_lazy("mood:notification_delete", args=[self.kwargs["id"]]), + "Delete Notification", + ) + ] return context def get_success_url(self): return reverse_lazy("mood:notification_list") def get_object(self): - return get_object_or_404(NotificationDailySchedule, notification__recipient=self.request.user, id=self.kwargs["id"]) + return get_object_or_404( + NotificationDailySchedule, + notification__recipient=self.request.user, + id=self.kwargs["id"], + ) class NotificationDeleteView(LoginRequiredMixin, DeleteView): @@ -315,7 +388,11 @@ class NotificationDeleteView(LoginRequiredMixin, DeleteView): model = NotificationDailySchedule def get_object(self): - return get_object_or_404(NotificationDailySchedule, notification__recipient=self.request.user, id=self.kwargs["id"]) + return get_object_or_404( + NotificationDailySchedule, + notification__recipient=self.request.user, + id=self.kwargs["id"], + ) def delete(self, request, *args, **kwargs): self.object = self.get_object() @@ -326,6 +403,7 @@ class NotificationDeleteView(LoginRequiredMixin, DeleteView): def get_success_url(self): return reverse_lazy("mood:notification_list") + class MoodStatisticsView(LoginRequiredMixin, TemplateView): template_name = "mood/statistics.html" @@ -348,6 +426,7 @@ class MoodStatisticsView(LoginRequiredMixin, TemplateView): context["activities"] = activitystats(self.request.user) return context + class MoodCSVView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): res = HttpResponse(content_type="text/csv") @@ -377,7 +456,9 @@ class MoodCSVView(LoginRequiredMixin, View): output = "date,value" - for status in Status.objects.filter(user=request.user, timestamp__gte=mindate, timestamp__lte=maxdate): + for status in Status.objects.filter( + user=request.user, timestamp__gte=mindate, timestamp__lte=maxdate + ): if status.mood: date = status.timestamp.strftime("%Y-%m-%d %H:%M") output += f"\n{date},{status.mood.value}" @@ -385,6 +466,7 @@ class MoodCSVView(LoginRequiredMixin, View): res.write(output) return res + class MoodPlotView(LoginRequiredMixin, View): @method_decorator(xframe_options_sameorigin) def dispatch(self, *args, **kwargs): @@ -396,6 +478,7 @@ class MoodPlotView(LoginRequiredMixin, View): res.write(hvhtml(moodstats(request.user))) return res + class MoodPiesView(LoginRequiredMixin, View): @method_decorator(xframe_options_sameorigin) def dispatch(self, *args, **kwargs): @@ -407,6 +490,7 @@ class MoodPiesView(LoginRequiredMixin, View): res.write(bkhtml(moodpies(request.user))) return res + class ActivityStatisticsView(LoginRequiredMixin, TemplateView): template_name = "mood/statistics_activity.html" @@ -416,6 +500,7 @@ class ActivityStatisticsView(LoginRequiredMixin, TemplateView): context["title"] = "Activity Statistics for %s" % activity.name return context + class ActivityPlotView(LoginRequiredMixin, View): @method_decorator(xframe_options_sameorigin) def dispatch(self, *args, **kwargs): @@ -424,9 +509,16 @@ class ActivityPlotView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): res = HttpResponse(content_type="text/html") - res.write(hvhtml(activitymood(get_object_or_404(Activity, user=request.user, id=kwargs["id"])))) + res.write( + hvhtml( + activitymood( + get_object_or_404(Activity, user=request.user, id=kwargs["id"]) + ) + ) + ) return res + class ActivityPiesView(LoginRequiredMixin, View): @method_decorator(xframe_options_sameorigin) def dispatch(self, *args, **kwargs): @@ -435,5 +527,50 @@ class ActivityPiesView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): res = HttpResponse(content_type="text/html") - res.write(bkhtml(activitypies(get_object_or_404(Activity, user=request.user, id=kwargs["id"])))) - return res \ No newline at end of file + res.write( + bkhtml( + activitypies( + get_object_or_404(Activity, user=request.user, id=kwargs["id"]) + ) + ) + ) + return res + + +class MoodCountHeatmapJSONView(LoginRequiredMixin, View): + """Returns a JSON object with the mood entries for a given time period. + + This is used in conjunction with the Cal-Heatmap library to display a + heatmap of mood entries. + """ + + def get(self, request, *args, **kwargs): + res = HttpResponse(content_type="application/json") + + start = request.GET.get("start") + end = request.GET.get("end") + + if end: + maxdate = datetime.strptime(end, "%Y-%m-%d") + else: + maxdate = timezone.now() + + if start: + mindate = datetime.strptime(start, "%Y-%m-%d") + else: + mindate = maxdate - relativedelta.relativedelta(years=1) + + data = ( + Status.objects.filter( + user=request.user, timestamp__gte=mindate, timestamp__lte=maxdate + + ) + .values("timestamp__date") + .annotate(value=Count("id")) + ) + + data = [{"date": d["timestamp__date"].strftime("%Y-%m-%d"), "value": d["value"]} for d in data] + + res.write(json.dumps(data)) + + return res