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.
This commit is contained in:
Kumi 2024-05-17 15:03:20 +02:00
parent 3e45ca5c2d
commit 9fbfe583be
11 changed files with 507 additions and 227 deletions

View file

@ -2,10 +2,12 @@
## General ## 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 - [_] User profiles / names / gateway config / time zone settings
- [_] Implement a proper logging system - [_] Implement a proper logging system
- [_] Implement a proper API - [_] Implement a proper API
- [_] Move dist files to separate directories
- [_] Turn this into issues
## cbt module ## cbt module

View file

@ -1,24 +1,32 @@
from django import template from django import template
from django.utils import timezone
from datetime import datetime, timedelta
from collections import Counter from collections import Counter
register = template.Library() register = template.Library()
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def total_dreams(context): def total_dreams(context):
return len(context["user"].dream_set.all()) return len(context["user"].dream_set.all())
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def weekly_dreams(context): def weekly_dreams(context):
now = timezone.now() now = datetime.now()
start = now - timezone.timedelta(days=7) 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) @register.simple_tag(takes_context=True)
def most_common_theme(context, start, end=None): 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() themes = list()
for dream in dream_list: for dream in dream_list:
@ -28,33 +36,32 @@ def most_common_theme(context, start, end=None):
try: try:
most_common = Counter(themes).most_common(1)[0] most_common = Counter(themes).most_common(1)[0]
return most_common[0], most_common[1] return most_common[0], most_common[1]
except: except Exception:
return None, None return None, None
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def most_common_theme_weekly(context): def most_common_theme_weekly(context):
now = timezone.now() now = datetime.now()
start = now - timezone.timedelta(days=7) start = now - timedelta(days=7)
return most_common_theme(context, start, now) return most_common_theme(context, start, now)
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def special_dreams(context, start, end=None): 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())) dream_list = context["user"].dream_set.filter(
wet = 0 timestamp__gte=start.date(),
lucid = 0 timestamp__lte=(end.date() if end else start.date()),
)
for dream in dream_list: lucid = sum([1 for dream in dream_list if dream.lucid])
if dream.lucid: wet = sum([1 for dream in dream_list if dream.wet])
lucid += 1
if dream.wet:
wet += 1
return lucid, wet return lucid, wet
@register.simple_tag(takes_context=True) @register.simple_tag(takes_context=True)
def special_dreams_weekly(context): def special_dreams_weekly(context):
now = timezone.now() now = datetime.now()
start = now - timezone.timedelta(days=7) start = now - timedelta(days=7)
return special_dreams(context, start, now) return special_dreams(context, start, now)

View file

@ -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()],
});

View file

@ -1,174 +1,221 @@
{% extends "frontend/base.html" %} {% extends "frontend/base.html" %} {% load mood_stats %} {% load dream_stats %}
{% load mood_stats %}
{% load dream_stats %}
{% block "content" %} {% block "content" %}
<!-- Mood stats --> <!-- Mood stats -->
<h2>Moods</h2>
<!-- Mood calendar heatmap -->
<div class="row"> <div class="row">
<div class="col-xl-12">
<!-- Earnings (Monthly) Card Example --> <div class="card">
<div class="col-xl-3 col-md-6 mb-4"> <div class="card-header">
<div class="card border-left-primary shadow h-100 py-2"> <h6 class="m-0 font-weight-bold text-primary">Mood Calendar</h6>
<div class="card-body"> </div>
<div class="row no-gutters align-items-center"> <div class="card-body">
<div class="col mr-2"> <div id="mood-count-heatmap"></div>
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1"> </div>
Status count (total)</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{% total_moods %}</div>
</div>
<div class="col-auto">
<i class="fas fa-book fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Earnings (Monthly) Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Current Streak Length</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{% current_streak %} <sub>days</sub></div>
</div>
<div class="col-auto">
<i class="fas fa-calendar fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Earnings (Monthly) Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Average mood (weekly)
</div>
<div class="row no-gutters align-items-center">
<div class="col-auto">
{% average_mood_weekly as mood %}
{% closest_mood mood as moodobj %}
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">{{ moodobj }} <sub><i>({{ mood|floatformat:2 }})</i></sub></div>
</div>
</div>
</div>
<div class="col-auto">
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Pending Requests Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Most Common Activity (weekly)</div>
{% most_common_activity_weekly as activity %}
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ activity.0 }} <sub><i>({{ activity.1 }})</i></sub></div>
</div>
<div class="col-auto">
<i class="{{ activity.0.icon }} fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div> </div>
</div>
</div> </div>
<!-- Mood cards -->
<div class="row">
<!-- Status count -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div
class="text-xs font-weight-bold text-primary text-uppercase mb-1"
>
Status count (total)
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{% total_moods %}
</div>
</div>
<div class="col-auto">
<i class="fas fa-book fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Current Streak Length -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div
class="text-xs font-weight-bold text-success text-uppercase mb-1"
>
Current Streak Length
</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">
{% current_streak %} <sub>days</sub>
</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Average mood -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Average mood (weekly)
</div>
<div class="row no-gutters align-items-center">
<div class="col-auto">
{% average_mood_weekly as mood %} {% closest_mood mood as moodobj %}
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">
{{ moodobj }} <sub><i>({{ mood|floatformat:2 }})</i></sub>
</div>
</div>
</div>
</div>
<div class="col-auto">
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Most common activity -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div
class="text-xs font-weight-bold text-warning text-uppercase mb-1"
>
Most Common Activity (weekly)
</div>
{% most_common_activity_weekly as activity %}
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ activity.0 }} <sub><i>({{ activity.1 }})</i></sub>
</div>
</div>
<div class="col-auto">
<i class="{{ activity.0.icon }} fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<h2>Dreams</h2>
<!-- Dream Stats --> <!-- Dream Stats -->
<div class="row"> <div class="row">
<!-- Total Dreams -->
<!-- Earnings (Monthly) Card Example --> <div class="col-xl-3 col-md-6 mb-4">
<div class="col-xl-3 col-md-6 mb-4"> <div class="card border-left-primary shadow h-100 py-2">
<div class="card border-left-primary shadow h-100 py-2"> <div class="card-body">
<div class="card-body"> <div class="row no-gutters align-items-center">
<div class="row no-gutters align-items-center"> <div class="col mr-2">
<div class="col mr-2"> <div
<div class="text-xs font-weight-bold text-primary text-uppercase mb-1"> class="text-xs font-weight-bold text-primary text-uppercase mb-1"
Dream count (total)</div> >
<div class="h5 mb-0 font-weight-bold text-gray-800">{% total_dreams %}</div> Dream count (total)
</div>
<div class="col-auto">
<i class="fas fa-book fa-2x text-gray-300"></i>
</div>
</div>
</div> </div>
</div> <div class="h5 mb-0 font-weight-bold text-gray-800">
</div> {% total_dreams %}
<!-- Earnings (Monthly) Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-success text-uppercase mb-1">
Dreams (weekly)</div>
<div class="h5 mb-0 font-weight-bold text-gray-800">{% weekly_dreams %}</div>
</div>
<div class="col-auto">
<i class="fas fa-calendar fa-2x text-gray-300"></i>
</div>
</div>
</div> </div>
</div>
<div class="col-auto">
<i class="fas fa-book fa-2x text-gray-300"></i>
</div>
</div> </div>
</div>
</div> </div>
</div>
<!-- Earnings (Monthly) Card Example --> <!-- Weekly Dreams -->
<div class="col-xl-3 col-md-6 mb-4"> <div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2"> <div class="card border-left-success shadow h-100 py-2">
<div class="card-body"> <div class="card-body">
<div class="row no-gutters align-items-center"> <div class="row no-gutters align-items-center">
<div class="col mr-2"> <div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">Special Dreams (lucid / wet) <div
</div> class="text-xs font-weight-bold text-success text-uppercase mb-1"
<div class="row no-gutters align-items-center"> >
<div class="col-auto"> Dreams (weekly)
{% special_dreams_weekly as weekly %}
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">{{ weekly.0 }} / {{ weekly.1 }} <sub><i>(weekly)</i></sub></div>
</div>
</div>
</div>
<div class="col-auto">
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
</div>
</div>
</div> </div>
</div> <div class="h5 mb-0 font-weight-bold text-gray-800">
</div> {% weekly_dreams %}
<!-- Pending Requests Card Example -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-warning text-uppercase mb-1">
Most Common Theme (weekly)</div>
{% most_common_theme_weekly as theme %}
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ theme.0 }} <sub><i>({{ theme.1 }})</i></sub></div>
</div>
<div class="col-auto">
<i class="fas fa-bed fa-2x text-gray-300"></i>
</div>
</div>
</div> </div>
</div>
<div class="col-auto">
<i class="fas fa-calendar fa-2x text-gray-300"></i>
</div>
</div> </div>
</div>
</div> </div>
</div>
<!-- Special Dreams -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
Special Dreams (lucid / wet)
</div>
<div class="row no-gutters align-items-center">
<div class="col-auto">
{% special_dreams_weekly as weekly %}
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">
{{ weekly.0 }} / {{ weekly.1 }} <sub><i>(weekly)</i></sub>
</div>
</div>
</div>
</div>
<div class="col-auto">
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<!-- Most common theme -->
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning shadow h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col mr-2">
<div
class="text-xs font-weight-bold text-warning text-uppercase mb-1"
>
Most Common Theme (weekly)
</div>
{% most_common_theme_weekly as theme %}
<div class="h5 mb-0 font-weight-bold text-gray-800">
{{ theme.0 }} <sub><i>({{ theme.1 }})</i></sub>
</div>
</div>
<div class="col-auto">
<i class="fas fa-bed fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock %}
{% endblock %}

View file

@ -11,7 +11,15 @@ class DashboardView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Dashboard" 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 return context
@ -26,4 +34,4 @@ class UserRegistrationView(CreateView):
return ret return ret
def get_success_url(self): def get_success_url(self):
return reverse_lazy("frontend:dashboard") return reverse_lazy("frontend:dashboard")

View file

@ -6,10 +6,9 @@ from django.utils import timezone
from math import pi from math import pi
from bokeh.models import HoverTool from bokeh.models import HoverTool
from bokeh.io import output_file, show
from bokeh.plotting import figure from bokeh.plotting import figure
from bokeh.transform import cumsum from bokeh.transform import cumsum
from bokeh.layouts import row, column from bokeh.layouts import column
from holoviews.operation import timeseries from holoviews.operation import timeseries
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta

View file

@ -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 from django.urls import path, include
app_name = "mood" app_name = "mood"
urlpatterns = [ urlpatterns = [
path('', StatusListView.as_view(), name="status_list"), path("", StatusListView.as_view(), name="status_list"),
path('status/<int:id>/view/', StatusViewView.as_view(), name="status_view"), path("status/<int:id>/view/", StatusViewView.as_view(), name="status_view"),
path('status/<int:id>/edit/', StatusEditView.as_view(), name="status_edit"), path("status/<int:id>/edit/", StatusEditView.as_view(), name="status_edit"),
path('status/<int:id>/delete/', StatusDeleteView.as_view(), name="status_delete"), path("status/<int:id>/delete/", StatusDeleteView.as_view(), name="status_delete"),
path('status/new/', StatusCreateView.as_view(), name="status_create"), path("status/new/", StatusCreateView.as_view(), name="status_create"),
path('activity/', ActivityListView.as_view(), name="activity_list"), path("activity/", ActivityListView.as_view(), name="activity_list"),
path('activity/<int:id>/edit/', ActivityEditView.as_view(), name="activity_edit"), path("activity/<int:id>/edit/", ActivityEditView.as_view(), name="activity_edit"),
path('activity/new/', ActivityCreateView.as_view(), name="activity_create"), path("activity/new/", ActivityCreateView.as_view(), name="activity_create"),
path('activity/<int:id>/delete/', ActivityDeleteView.as_view(), name="activity_delete"), path(
path('mood/', MoodListView.as_view(), name="mood_list"), "activity/<int:id>/delete/",
path('mood/<int:id>/edit/', MoodEditView.as_view(), name="mood_edit"), ActivityDeleteView.as_view(),
path('notification/', NotificationListView.as_view(), name="notification_list"), name="activity_delete",
path('notification/<int:id>/edit/', NotificationEditView.as_view(), name="notification_edit"), ),
path('notification/<int:id>/delete/', NotificationDeleteView.as_view(), name="notification_delete"), path("mood/", MoodListView.as_view(), name="mood_list"),
path('notification/new/', NotificationCreateView.as_view(), name="notification_create"), path("mood/<int:id>/edit/", MoodEditView.as_view(), name="mood_edit"),
path('statistics/', MoodStatisticsView.as_view(), name="statistics"), path("notification/", NotificationListView.as_view(), name="notification_list"),
path('statistics/csv/', MoodCSVView.as_view(), name="statistics_csv"), path(
path('statistics/plot/', MoodPlotView.as_view(), name="statistics_plot"), "notification/<int:id>/edit/",
path('statistics/pies/', MoodPiesView.as_view(), name="statistics_pies"), NotificationEditView.as_view(),
path('statistics/activity/<int:id>/', ActivityStatisticsView.as_view(), name="statistics_activity"), name="notification_edit",
path('statistics/activity/<int:id>/plot/', ActivityPlotView.as_view(), name="statistics_activity_plot"), ),
path('statistics/activity/<int:id>/pies/', ActivityPiesView.as_view(), name="statistics_activity_pies"), path(
] "notification/<int:id>/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/<int:id>/",
ActivityStatisticsView.as_view(),
name="statistics_activity",
),
path(
"statistics/activity/<int:id>/plot/",
ActivityPlotView.as_view(),
name="statistics_activity_plot",
),
path(
"statistics/activity/<int:id>/pies/",
ActivityPiesView.as_view(),
name="statistics_activity_pies",
),
]

View file

@ -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.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse_lazy from django.urls import reverse_lazy
@ -6,6 +14,7 @@ from django.http import HttpResponseRedirect, HttpResponse
from django.utils import timezone from django.utils import timezone
from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.db.models import Count
from .models import Status, Activity, Mood, StatusMedia, StatusActivity from .models import Status, Activity, Mood, StatusMedia, StatusActivity
from .forms import StatusForm from .forms import StatusForm
@ -19,6 +28,9 @@ from dateutil import relativedelta
from datetime import datetime from datetime import datetime
import json
class StatusListView(LoginRequiredMixin, ListView): class StatusListView(LoginRequiredMixin, ListView):
template_name = "mood/status_list.html" template_name = "mood/status_list.html"
model = Status model = Status
@ -27,11 +39,13 @@ class StatusListView(LoginRequiredMixin, ListView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Status List" context["title"] = "Status List"
context["subtitle"] = "Just a list of your mood entries." 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 return context
def get_queryset(self): 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): class StatusViewView(LoginRequiredMixin, DetailView):
@ -42,7 +56,13 @@ class StatusViewView(LoginRequiredMixin, DetailView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "View Status" context["title"] = "View Status"
context["subtitle"] = "View the details of your mood entry." 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 return context
def get_object(self): def get_object(self):
@ -91,7 +111,13 @@ class StatusEditView(LoginRequiredMixin, UpdateView):
context["title"] = "Update Status" context["title"] = "Update Status"
context["subtitle"] = "Change a status you created before." context["subtitle"] = "Change a status you created before."
context["scripts"] = ["frontend/js/dropdown-to-buttons.js"] 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 return context
def get_object(self): def get_object(self):
@ -102,11 +128,13 @@ class StatusEditView(LoginRequiredMixin, UpdateView):
dba = StatusMedia(status=form.instance) dba = StatusMedia(status=form.instance)
dba.file.save(get_upload_path(form.instance, attachment.name), attachment) dba.file.save(get_upload_path(form.instance, attachment.name), attachment)
dba.save() dba.save()
for activity in form.cleaned_data["activities"]: for activity in form.cleaned_data["activities"]:
if activity.user == self.request.user: if activity.user == self.request.user:
if not activity in form.instance.activity_set: 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(): for statusactivity in form.instance.statusactivity_set.all():
if not statusactivity.activity in form.cleaned_data["activities"]: if not statusactivity.activity in form.cleaned_data["activities"]:
@ -137,7 +165,9 @@ class ActivityListView(LoginRequiredMixin, ListView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Activities" context["title"] = "Activities"
context["subtitle"] = "The activities you have defined." 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 return context
def get_queryset(self): def get_queryset(self):
@ -153,9 +183,20 @@ class ActivityEditView(LoginRequiredMixin, UpdateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Edit Activity" context["title"] = "Edit Activity"
context["subtitle"] = "Make changes to the 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["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 return context
def get_object(self): def get_object(self):
@ -174,7 +215,12 @@ class ActivityCreateView(LoginRequiredMixin, CreateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Create Activity" context["title"] = "Create Activity"
context["subtitle"] = "Add a new 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"] context["styles"] = ["frontend/css/fontawesome-iconpicker.min.css"]
return context return context
@ -221,7 +267,12 @@ class MoodEditView(LoginRequiredMixin, UpdateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Edit Mood" context["title"] = "Edit Mood"
context["subtitle"] = "Make changes to the 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"] context["styles"] = ["frontend/css/fontawesome-iconpicker.min.css"]
return context return context
@ -241,7 +292,12 @@ class MoodCreateView(LoginRequiredMixin, CreateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Create Activity" context["title"] = "Create Activity"
context["subtitle"] = "Add a new 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"] context["styles"] = ["frontend/css/fontawesome-iconpicker.min.css"]
return context return context
@ -263,11 +319,15 @@ class NotificationListView(LoginRequiredMixin, ListView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Notifications" context["title"] = "Notifications"
context["subtitle"] = "The daily reminders you have set up." 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 return context
def get_queryset(self): 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): class NotificationCreateView(LoginRequiredMixin, CreateView):
@ -282,7 +342,11 @@ class NotificationCreateView(LoginRequiredMixin, CreateView):
return context return context
def form_valid(self, form): 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 = form.save(commit=False)
obj.notification = notification obj.notification = notification
return super().form_valid(form) return super().form_valid(form)
@ -300,14 +364,23 @@ class NotificationEditView(LoginRequiredMixin, UpdateView):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["title"] = "Edit Notification" context["title"] = "Edit Notification"
context["subtitle"] = "Change the time of a daily 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 return context
def get_success_url(self): def get_success_url(self):
return reverse_lazy("mood:notification_list") return reverse_lazy("mood:notification_list")
def get_object(self): 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): class NotificationDeleteView(LoginRequiredMixin, DeleteView):
@ -315,7 +388,11 @@ class NotificationDeleteView(LoginRequiredMixin, DeleteView):
model = NotificationDailySchedule model = NotificationDailySchedule
def get_object(self): 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): def delete(self, request, *args, **kwargs):
self.object = self.get_object() self.object = self.get_object()
@ -326,6 +403,7 @@ class NotificationDeleteView(LoginRequiredMixin, DeleteView):
def get_success_url(self): def get_success_url(self):
return reverse_lazy("mood:notification_list") return reverse_lazy("mood:notification_list")
class MoodStatisticsView(LoginRequiredMixin, TemplateView): class MoodStatisticsView(LoginRequiredMixin, TemplateView):
template_name = "mood/statistics.html" template_name = "mood/statistics.html"
@ -348,6 +426,7 @@ class MoodStatisticsView(LoginRequiredMixin, TemplateView):
context["activities"] = activitystats(self.request.user) context["activities"] = activitystats(self.request.user)
return context return context
class MoodCSVView(LoginRequiredMixin, View): class MoodCSVView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
res = HttpResponse(content_type="text/csv") res = HttpResponse(content_type="text/csv")
@ -377,7 +456,9 @@ class MoodCSVView(LoginRequiredMixin, View):
output = "date,value" 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: if status.mood:
date = status.timestamp.strftime("%Y-%m-%d %H:%M") date = status.timestamp.strftime("%Y-%m-%d %H:%M")
output += f"\n{date},{status.mood.value}" output += f"\n{date},{status.mood.value}"
@ -385,6 +466,7 @@ class MoodCSVView(LoginRequiredMixin, View):
res.write(output) res.write(output)
return res return res
class MoodPlotView(LoginRequiredMixin, View): class MoodPlotView(LoginRequiredMixin, View):
@method_decorator(xframe_options_sameorigin) @method_decorator(xframe_options_sameorigin)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
@ -396,6 +478,7 @@ class MoodPlotView(LoginRequiredMixin, View):
res.write(hvhtml(moodstats(request.user))) res.write(hvhtml(moodstats(request.user)))
return res return res
class MoodPiesView(LoginRequiredMixin, View): class MoodPiesView(LoginRequiredMixin, View):
@method_decorator(xframe_options_sameorigin) @method_decorator(xframe_options_sameorigin)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
@ -407,6 +490,7 @@ class MoodPiesView(LoginRequiredMixin, View):
res.write(bkhtml(moodpies(request.user))) res.write(bkhtml(moodpies(request.user)))
return res return res
class ActivityStatisticsView(LoginRequiredMixin, TemplateView): class ActivityStatisticsView(LoginRequiredMixin, TemplateView):
template_name = "mood/statistics_activity.html" template_name = "mood/statistics_activity.html"
@ -416,6 +500,7 @@ class ActivityStatisticsView(LoginRequiredMixin, TemplateView):
context["title"] = "Activity Statistics for %s" % activity.name context["title"] = "Activity Statistics for %s" % activity.name
return context return context
class ActivityPlotView(LoginRequiredMixin, View): class ActivityPlotView(LoginRequiredMixin, View):
@method_decorator(xframe_options_sameorigin) @method_decorator(xframe_options_sameorigin)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
@ -424,9 +509,16 @@ class ActivityPlotView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
res = HttpResponse(content_type="text/html") 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 return res
class ActivityPiesView(LoginRequiredMixin, View): class ActivityPiesView(LoginRequiredMixin, View):
@method_decorator(xframe_options_sameorigin) @method_decorator(xframe_options_sameorigin)
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
@ -435,5 +527,50 @@ class ActivityPiesView(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
res = HttpResponse(content_type="text/html") res = HttpResponse(content_type="text/html")
res.write(bkhtml(activitypies(get_object_or_404(Activity, user=request.user, id=kwargs["id"])))) res.write(
return res 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