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:
parent
3e45ca5c2d
commit
9fbfe583be
11 changed files with 507 additions and 227 deletions
4
TODO.md
4
TODO.md
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)
|
27
frontend/static/frontend/js/dashboard.js
Normal file
27
frontend/static/frontend/js/dashboard.js
Normal 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()],
|
||||||
|
});
|
|
@ -1,20 +1,39 @@
|
||||||
{% 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 -->
|
||||||
<div class="row">
|
<h2>Moods</h2>
|
||||||
|
|
||||||
<!-- Earnings (Monthly) Card Example -->
|
<!-- Mood calendar heatmap -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xl-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="m-0 font-weight-bold text-primary">Mood Calendar</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="mood-count-heatmap"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mood cards -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Status count -->
|
||||||
<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 class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
<div
|
||||||
Status count (total)</div>
|
class="text-xs font-weight-bold text-primary text-uppercase mb-1"
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{% total_moods %}</div>
|
>
|
||||||
|
Status count (total)
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
|
{% total_moods %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="fas fa-book fa-2x text-gray-300"></i>
|
<i class="fas fa-book fa-2x text-gray-300"></i>
|
||||||
|
@ -24,15 +43,20 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Earnings (Monthly) Card Example -->
|
<!-- Current Streak Length -->
|
||||||
<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-success 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-success text-uppercase mb-1">
|
<div
|
||||||
Current Streak Length</div>
|
class="text-xs font-weight-bold text-success text-uppercase mb-1"
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{% current_streak %} <sub>days</sub></div>
|
>
|
||||||
|
Current Streak Length
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
|
{% current_streak %} <sub>days</sub>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="fas fa-calendar fa-2x text-gray-300"></i>
|
<i class="fas fa-calendar fa-2x text-gray-300"></i>
|
||||||
|
@ -42,41 +66,47 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Earnings (Monthly) Card Example -->
|
<!-- Average mood -->
|
||||||
<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-info 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">Average mood (weekly)
|
<div class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||||
|
Average mood (weekly)
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters align-items-center">
|
<div class="row no-gutters align-items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
{% average_mood_weekly as mood %}
|
{% average_mood_weekly as mood %} {% closest_mood mood as moodobj %}
|
||||||
{% closest_mood mood as moodobj %}
|
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">
|
||||||
<div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">{{ moodobj }} <sub><i>({{ mood|floatformat:2 }})</i></sub></div>
|
{{ moodobj }} <sub><i>({{ mood|floatformat:2 }})</i></sub>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
|
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pending Requests Card Example -->
|
<!-- Most common activity -->
|
||||||
<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-warning shadow h-100 py-2">
|
<div class="card border-left-warning 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-warning text-uppercase mb-1">
|
<div
|
||||||
Most Common Activity (weekly)</div>
|
class="text-xs font-weight-bold text-warning text-uppercase mb-1"
|
||||||
|
>
|
||||||
|
Most Common Activity (weekly)
|
||||||
|
</div>
|
||||||
{% most_common_activity_weekly as activity %}
|
{% 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 class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
|
{{ activity.0 }} <sub><i>({{ activity.1 }})</i></sub>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="{{ activity.0.icon }} fa-2x text-gray-300"></i>
|
<i class="{{ activity.0.icon }} fa-2x text-gray-300"></i>
|
||||||
|
@ -87,18 +117,24 @@
|
||||||
</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 class="text-xs font-weight-bold text-primary text-uppercase mb-1">
|
<div
|
||||||
Dream count (total)</div>
|
class="text-xs font-weight-bold text-primary text-uppercase mb-1"
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{% total_dreams %}</div>
|
>
|
||||||
|
Dream count (total)
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
|
{% total_dreams %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="fas fa-book fa-2x text-gray-300"></i>
|
<i class="fas fa-book fa-2x text-gray-300"></i>
|
||||||
|
@ -108,15 +144,20 @@
|
||||||
</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-success 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-success text-uppercase mb-1">
|
<div
|
||||||
Dreams (weekly)</div>
|
class="text-xs font-weight-bold text-success text-uppercase mb-1"
|
||||||
<div class="h5 mb-0 font-weight-bold text-gray-800">{% weekly_dreams %}</div>
|
>
|
||||||
|
Dreams (weekly)
|
||||||
|
</div>
|
||||||
|
<div class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
|
{% weekly_dreams %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="fas fa-calendar fa-2x text-gray-300"></i>
|
<i class="fas fa-calendar fa-2x text-gray-300"></i>
|
||||||
|
@ -126,40 +167,47 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Earnings (Monthly) Card Example -->
|
<!-- Special 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-info 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 class="text-xs font-weight-bold text-info text-uppercase mb-1">
|
||||||
|
Special Dreams (lucid / wet)
|
||||||
</div>
|
</div>
|
||||||
<div class="row no-gutters align-items-center">
|
<div class="row no-gutters align-items-center">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
{% special_dreams_weekly as 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 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>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
|
<i class="{{ moodobj.icon }} fa-2x text-gray-300"></i>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pending Requests Card Example -->
|
<!-- Most common theme -->
|
||||||
<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-warning shadow h-100 py-2">
|
<div class="card border-left-warning 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-warning text-uppercase mb-1">
|
<div
|
||||||
Most Common Theme (weekly)</div>
|
class="text-xs font-weight-bold text-warning text-uppercase mb-1"
|
||||||
|
>
|
||||||
|
Most Common Theme (weekly)
|
||||||
|
</div>
|
||||||
{% most_common_theme_weekly as theme %}
|
{% 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 class="h5 mb-0 font-weight-bold text-gray-800">
|
||||||
|
{{ theme.0 }} <sub><i>({{ theme.1 }})</i></sub>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<i class="fas fa-bed fa-2x text-gray-300"></i>
|
<i class="fas fa-bed fa-2x text-gray-300"></i>
|
||||||
|
@ -170,5 +218,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
99
mood/urls.py
99
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
|
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",
|
||||||
|
),
|
||||||
]
|
]
|
179
mood/views.py
179
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.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):
|
||||||
|
@ -106,7 +132,9 @@ class StatusEditView(LoginRequiredMixin, UpdateView):
|
||||||
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(
|
||||||
|
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
|
return res
|
Loading…
Reference in a new issue