This commit is contained in:
2018-12-27 04:30:12 +04:00
parent 48b4c9bbfe
commit 273afd18d0
24 changed files with 374 additions and 116 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.sqlite3
__pycache__

Binary file not shown.

View File

@@ -1,15 +1,10 @@
<!DOCTYPE html> {% extends 'web/base.html' %}
<html lang="en">
<head> {% block main %}
<meta charset="UTF-8">
<title>Новая запись</title>
</head>
<body>
<form method="post">{% csrf_token %} <form method="post">{% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<input type="submit" value="Сохранить"> <input class="btn btn-primary" type="submit" value="Сохранить">
</form> </form>
</body> {% endblock %}
</html>

39
templates/web/base.html Normal file
View File

@@ -0,0 +1,39 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{% block title %}
LifeStats
{% endblock %}
</title>
<link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"
integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"
integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q"
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl"
crossorigin="anonymous"></script>
{% block head %}
{% endblock %}
</head>
<body>
{% block body %}
<div class="container-fluid">
{% block main %}
{% endblock %}
</div>
{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,28 @@
{% extends 'web/base.html' %}
{% block main %}
<div id='breadcrumbs'>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'users' %}">
Главная
</a>
</li>
<li class="breadcrumb-item">
<a href="{% url 'user' user.id %}">
{{ user.name }}
</a>
</li>
</ol>
</div>
<h2>
{{ user }} — {{ activity }}
</h2>
<img src="{% url 'user_charts_activity_all' user.id activity.id %}"/>
{# <img src="{% url 'user_charts_activity_tracker' user.id activity.id %}"/>#}
{% endblock %}

View File

@@ -1,36 +1,49 @@
<!DOCTYPE html> {% extends 'web/base.html' %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Пользователь</title>
</head>
<body>
<h1>{{ user }}</h1> {% block main %}
<div id='breadcrumbs'>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="{% url 'users' %}">
Главная
</a>
</li>
</ol>
</div>
<h1>{{ user }}</h1>
<h2>Записи</h2> <div class="row">
<div class="col-sm">
<h2>Записи</h2>
<a href="{% url 'activity_log_create' user.id %}">Создать новую запись</a> <a class="btn btn-primary" href="{% url 'activity_log_create' user.id %}">
Создать новую запись
</a>
{% if activity_logs %} {% if activity_logs %}
<table> <table class="table">
<thead>
<tr> <tr>
<td> <th>
Активность Активность
</td> </th>
<td> <th>
Когда начал Когда начал
</td> </th>
<td> <th>
Когда закончил Когда закончил
</td> </th>
<td> <th>
</td> </th>
<td> <th>
</td> </th>
</tr> </tr>
</thead>
<tbody>
{% for activity_log in activity_logs %} {% for activity_log in activity_logs %}
<tr> <tr>
<td> <td>
@@ -43,29 +56,45 @@
{{ activity_log.end_time }} {{ activity_log.end_time }}
</td> </td>
<td> <td>
<form method="get" action="{% url 'activity_log_update' user.id activity_log.id %}">{% csrf_token %} <form method="get" action="{% url 'activity_log_update' user.id activity_log.id %}">{% csrf_token %}
<input type="submit" value="изменить"> <input class="btn btn-outline-primary" type="submit" value="изменить">
</form> </form>
</td> </td>
<td> <td>
<form method="post" action="{% url 'activity_log_delete' user.id activity_log.id %}">{% csrf_token %} <form method="post" action="{% url 'activity_log_delete' user.id activity_log.id %}">{% csrf_token %}
<input type="submit" value="удалить"> <input class="btn btn-outline-primary" type="submit" value="удалить">
</form> </form>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody>
</table> </table>
{% else %} {% else %}
<li>Ничего не залогано :(</li> <li>Ничего не залогано :(</li>
{% endif %} {% endif %}
</div>
<div class="col-sm">
<img src="{% url 'user_charts_pie' user.id %}"/>
<h2>Активности</h2>
<ul>
{% for activity in activities %}
<li>
<a href="{% url 'user_charts_activity' user.id activity.id %}">
{{ activity }}
</a>
</li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}
</table>
</body>
</html>

View File

@@ -1,15 +1,10 @@
<!DOCTYPE html> {% extends 'web/base.html' %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Новый пользователь</title>
</head>
<body>
<form method="post">{% csrf_token %} {% block main %}
<form method="post">{% csrf_token %}
{{ form.as_p }} {{ form.as_p }}
<input type="submit" value="Сохранить"> <input class="btn btn-primary" type="submit" value="Сохранить">
</form> </form>
</body> {% endblock %}
</html>

View File

@@ -1,13 +1,9 @@
<!DOCTYPE html> {% extends 'web/base.html' %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Пользователи</title>
</head>
<body>
<h1>Пользователи</h1> {% block main %}
<ul>
<h1>Пользователи</h1>
<ul>
{% for user in object_list %} {% for user in object_list %}
<li> <li>
<a href="{% url 'user' user.id %}"> <a href="{% url 'user' user.id %}">
@@ -17,12 +13,11 @@
{% empty %} {% empty %}
<li>Нет пользователей :(</li> <li>Нет пользователей :(</li>
{% endfor %} {% endfor %}
</ul> </ul>
<a href="{% url 'users_new' %}"> <a href="{% url 'users_new' %}" class="btn btn-primary">
Создать нового пользователя Создать нового пользователя
</a> </a>
</body> {% endblock %}
</html>

View File

@@ -101,7 +101,7 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization # Internationalization
# https://docs.djangoproject.com/en/2.1/topics/i18n/ # https://docs.djangoproject.com/en/2.1/topics/i18n/
LANGUAGE_CODE = 'en-us' LANGUAGE_CODE = 'ru-ru'
TIME_ZONE = 'UTC' TIME_ZONE = 'UTC'
@@ -115,3 +115,5 @@ USE_TZ = False
# https://docs.djangoproject.com/en/2.1/howto/static-files/ # https://docs.djangoproject.com/en/2.1/howto/static-files/
STATIC_URL = '/static/' STATIC_URL = '/static/'
# DATETIME_FORMAT = ''

Binary file not shown.

Binary file not shown.

Binary file not shown.

148
web/chart_views.py Normal file
View File

@@ -0,0 +1,148 @@
import io
from collections.__init__ import defaultdict
from datetime import timedelta
import numpy as np
from django.http import HttpResponse
from django.views.generic import DetailView
from web.models import User, ActivityLog, Activity
class UserChartsPie(DetailView):
model = User
pk_url_kwarg = 'user_id'
def render_to_response(self, context, **response_kwargs):
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(6, 6), dpi=100)
ax = fig.add_subplot(111)
total_time = defaultdict(float)
logs = ActivityLog.objects.filter(user=self.object).all()
for log in logs:
total_time[str(log.activity)] += (log.end_time - log.start_time).total_seconds()
sectors = sorted(list(total_time.items()), key=lambda x: x[1])
ax.pie(x=[i[1] for i in sectors], labels=[i[0] for i in sectors])
buf = io.BytesIO()
fig.savefig(buf, format='png')
b = buf.getvalue()
return HttpResponse(b, content_type='image/png')
class UserChartsActivityAll(DetailView):
model = User
pk_url_kwarg = 'user_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['activity'] = Activity.objects.filter(id=self.kwargs['activity_id']).first()
return context
def bar_chart(self, plt, ax, logs, day_l, day_r, day_count, days):
day_seconds = np.zeros(day_count)
for log in logs:
for i, day in enumerate(days):
l = max(log.start_time, day)
r = min(log.end_time, day + timedelta(days=1))
if r > l:
day_seconds[i] += (r - l).total_seconds()
day_hours = day_seconds / timedelta(hours=1).total_seconds()
ax.bar(days, day_hours)
plt.xticks(days, [f'{i:%m-%d}' for i in days], rotation=60)
plt.ylabel('кол-во часов')
def tracker_chart(self, plt, ax, logs, day_l, day_r, day_count, days):
ys = []
widths = []
lefts = []
for log in logs:
for i, day in enumerate(days):
l = max(log.start_time, day)
r = min(log.end_time, day + timedelta(days=1))
if r > l:
ys.append(day)
widths.append((r - l) / timedelta(hours=1))
lefts.append((l - day) / timedelta(hours=1))
ax.barh(y=ys, width=widths, left=lefts, zorder=2)
plt.xlim(0, 24)
plt.xticks(range(24), [f'{i:02d}:00' for i in range(24)])
plt.yticks(days, [f'{i:%m-%d}' for i in days])
plt.grid(True, zorder=1)
def render_to_response(self, context, **response_kwargs):
logs = list(ActivityLog.objects.filter(user=self.object, activity=context['activity']).order_by('start_time').all())
day_r = logs[-1].end_time.replace(hour=0, minute=0, second=0, microsecond=0)
day_l = max(
logs[0].start_time.replace(hour=0, minute=0, second=0, microsecond=0),
day_r - timedelta(days=14)
)
day_count = (day_r - day_l) // timedelta(days=1) + 1
days = [day_l + timedelta(days=1) * i for i in range(day_count)]
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(16, 12), dpi=100)
ax = fig.add_subplot(211)
self.bar_chart(plt, ax, logs, day_l, day_r, day_count, days)
ax = fig.add_subplot(212)
self.tracker_chart(plt, ax, logs, day_l, day_r, day_count, days)
buf = io.BytesIO()
fig.savefig(buf, format='png')
b = buf.getvalue()
return HttpResponse(b, content_type='image/png')
class UserActivityChartsTracker(DetailView):
model = User
pk_url_kwarg = 'user_id'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['activity'] = Activity.objects.filter(id=self.kwargs['activity_id']).first()
return context
def render_to_response(self, context, **response_kwargs):
print('start_tracker')
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
fig = plt.figure(figsize=(16, 6), dpi=100)
ax = fig.add_subplot(111)
logs = list(ActivityLog.objects.filter(user=self.object, activity=context['activity']).order_by('start_time').all())
day_r = logs[-1].end_time.replace(hour=0, minute=0, second=0, microsecond=0)
day_l = max(
logs[0].start_time.replace(hour=0, minute=0, second=0, microsecond=0),
day_r - timedelta(days=14)
)
day_count = (day_r - day_l) // timedelta(days=1) + 1
days = [day_l + timedelta(days=1) * i for i in range(day_count)]
buf = io.BytesIO()
fig.savefig(buf, format='png')
b = buf.getvalue()
return HttpResponse(b, content_type='image/png')

View File

@@ -3,8 +3,8 @@ from django.db import models
class User(models.Model): class User(models.Model):
name = models.TextField() name = models.TextField(verbose_name='Имя')
email = models.TextField() email = models.TextField(verbose_name='Почта')
def __str__(self): def __str__(self):
return f'{self.name}' return f'{self.name}'
@@ -18,11 +18,11 @@ class Activity(models.Model):
class ActivityLog(models.Model): class ActivityLog(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь')
activity = models.ForeignKey(Activity, on_delete=models.CASCADE) activity = models.ForeignKey(Activity, on_delete=models.CASCADE, verbose_name='Тип активности')
start_time = models.DateTimeField() start_time = models.DateTimeField(verbose_name='Момент начала')
end_time = models.DateTimeField() end_time = models.DateTimeField(verbose_name='Момент окончания')
logged_at = models.DateTimeField(auto_now_add=True) logged_at = models.DateTimeField(auto_now_add=True)
def clean(self): def clean(self):

View File

@@ -1,5 +1,6 @@
from django.urls import path from django.urls import path
from web import chart_views
from web import views from web import views
urlpatterns = [ urlpatterns = [
@@ -16,4 +17,13 @@ urlpatterns = [
views.ActivityLogUpdateView.as_view(), name='activity_log_update'), views.ActivityLogUpdateView.as_view(), name='activity_log_update'),
path('users/<int:user_id>/activity_log/<int:activity_log_id>/delete', path('users/<int:user_id>/activity_log/<int:activity_log_id>/delete',
views.ActivityLogDeleteView.as_view(), name='activity_log_delete'), views.ActivityLogDeleteView.as_view(), name='activity_log_delete'),
path('users/<int:user_id>/charts/pie',
chart_views.UserChartsPie.as_view(), name='user_charts_pie'),
path('users/<int:user_id>/charts/activity/<int:activity_id>',
views.UserChartsActivity.as_view(), name='user_charts_activity'),
path('users/<int:user_id>/charts/activity/<int:activity_id>/all',
chart_views.UserChartsActivityAll.as_view(), name='user_charts_activity_all'),
# path('users/<int:user_id>/charts/activity/<int:activity_id>/tracker',
# chart_views.UserActivityChartsTracker.as_view(), name='user_charts_activity_tracker'),
] ]

View File

@@ -1,7 +1,7 @@
from django.urls import reverse_lazy, reverse from django.urls import reverse, reverse_lazy
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView from django.views.generic import CreateView, DeleteView, DetailView, ListView, UpdateView
from web.models import Activity, User, ActivityLog from web.models import ActivityLog, User, Activity
class UserListView(ListView): class UserListView(ListView):
@@ -22,6 +22,7 @@ class UserDetailView(DetailView):
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['activity_logs'] = ActivityLog.objects.filter(user=self.object).order_by('start_time') context['activity_logs'] = ActivityLog.objects.filter(user=self.object).order_by('start_time')
context['activities'] = set(i.activity for i in ActivityLog.objects.filter(user=self.object))
return context return context
@@ -54,3 +55,16 @@ class ActivityLogDeleteView(DeleteView):
return reverse('user', kwargs={ return reverse('user', kwargs={
'user_id': self.object.user.id, 'user_id': self.object.user.id,
}) })
class UserChartsActivity(DetailView):
model = User
pk_url_kwarg = 'user_id'
template_name_suffix = '_activity_charts'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['activity'] = Activity.objects.filter(id=self.kwargs['activity_id']).first()
return context