com2
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*.sqlite3
|
||||
|
||||
__pycache__
|
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
@@ -1,15 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Новая запись</title>
|
||||
</head>
|
||||
<body>
|
||||
{% extends 'web/base.html' %}
|
||||
|
||||
{% block main %}
|
||||
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Сохранить">
|
||||
<input class="btn btn-primary" type="submit" value="Сохранить">
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
39
templates/web/base.html
Normal file
39
templates/web/base.html
Normal 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>
|
28
templates/web/user_activity_charts.html
Normal file
28
templates/web/user_activity_charts.html
Normal 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 %}
|
@@ -1,71 +1,100 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Пользователь</title>
|
||||
</head>
|
||||
<body>
|
||||
{% extends 'web/base.html' %}
|
||||
|
||||
<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 %}
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
Активность
|
||||
</td>
|
||||
<td>
|
||||
Когда начал
|
||||
</td>
|
||||
<td>
|
||||
Когда закончил
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
{% if activity_logs %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
Активность
|
||||
</th>
|
||||
<th>
|
||||
Когда начал
|
||||
</th>
|
||||
<th>
|
||||
Когда закончил
|
||||
</th>
|
||||
<th>
|
||||
</th>
|
||||
<th>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{% for activity_log in activity_logs %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ activity_log.activity }}
|
||||
</td>
|
||||
<td>
|
||||
{{ activity_log.start_time }}
|
||||
</td>
|
||||
<td>
|
||||
{{ activity_log.end_time }}
|
||||
</td>
|
||||
<td>
|
||||
<form method="get" action="{% url 'activity_log_update' user.id activity_log.id %}">{% csrf_token %}
|
||||
<input type="submit" value="изменить">
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{% url 'activity_log_delete' user.id activity_log.id %}">{% csrf_token %}
|
||||
<input type="submit" value="удалить">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% else %}
|
||||
<li>Ничего не залогано :(</li>
|
||||
{% endif %}
|
||||
<tbody>
|
||||
{% for activity_log in activity_logs %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ activity_log.activity }}
|
||||
</td>
|
||||
<td>
|
||||
{{ activity_log.start_time }}
|
||||
</td>
|
||||
<td>
|
||||
{{ activity_log.end_time }}
|
||||
</td>
|
||||
<td>
|
||||
|
||||
|
||||
<form method="get" action="{% url 'activity_log_update' user.id activity_log.id %}">{% csrf_token %}
|
||||
<input class="btn btn-outline-primary" type="submit" value="изменить">
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="{% url 'activity_log_delete' user.id activity_log.id %}">{% csrf_token %}
|
||||
<input class="btn btn-outline-primary" type="submit" value="удалить">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<li>Ничего не залогано :(</li>
|
||||
{% 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>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
@@ -1,15 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Новый пользователь</title>
|
||||
</head>
|
||||
<body>
|
||||
{% extends 'web/base.html' %}
|
||||
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input type="submit" value="Сохранить">
|
||||
</form>
|
||||
{% block main %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<form method="post">{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input class="btn btn-primary" type="submit" value="Сохранить">
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
|
@@ -1,28 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Пользователи</title>
|
||||
</head>
|
||||
<body>
|
||||
{% extends 'web/base.html' %}
|
||||
|
||||
<h1>Пользователи</h1>
|
||||
<ul>
|
||||
{% for user in object_list %}
|
||||
<li>
|
||||
<a href="{% url 'user' user.id %}">
|
||||
{{ user.name }} - {{ user.email }}
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>Нет пользователей :(</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% block main %}
|
||||
|
||||
<h1>Пользователи</h1>
|
||||
<ul>
|
||||
{% for user in object_list %}
|
||||
<li>
|
||||
<a href="{% url 'user' user.id %}">
|
||||
{{ user.name }} - {{ user.email }}
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li>Нет пользователей :(</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
|
||||
<a href="{% url 'users_new' %}">
|
||||
Создать нового пользователя
|
||||
</a>
|
||||
<a href="{% url 'users_new' %}" class="btn btn-primary">
|
||||
Создать нового пользователя
|
||||
</a>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
{% endblock %}
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -101,7 +101,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/2.1/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
LANGUAGE_CODE = 'ru-ru'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
@@ -115,3 +115,5 @@ USE_TZ = False
|
||||
# https://docs.djangoproject.com/en/2.1/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# DATETIME_FORMAT = ''
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
148
web/chart_views.py
Normal file
148
web/chart_views.py
Normal 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')
|
Binary file not shown.
Binary file not shown.
@@ -3,8 +3,8 @@ from django.db import models
|
||||
|
||||
|
||||
class User(models.Model):
|
||||
name = models.TextField()
|
||||
email = models.TextField()
|
||||
name = models.TextField(verbose_name='Имя')
|
||||
email = models.TextField(verbose_name='Почта')
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}'
|
||||
@@ -18,11 +18,11 @@ class Activity(models.Model):
|
||||
|
||||
|
||||
class ActivityLog(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
activity = models.ForeignKey(Activity, on_delete=models.CASCADE)
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='Пользователь')
|
||||
activity = models.ForeignKey(Activity, on_delete=models.CASCADE, verbose_name='Тип активности')
|
||||
|
||||
start_time = models.DateTimeField()
|
||||
end_time = models.DateTimeField()
|
||||
start_time = models.DateTimeField(verbose_name='Момент начала')
|
||||
end_time = models.DateTimeField(verbose_name='Момент окончания')
|
||||
logged_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def clean(self):
|
||||
|
10
web/urls.py
10
web/urls.py
@@ -1,5 +1,6 @@
|
||||
from django.urls import path
|
||||
|
||||
from web import chart_views
|
||||
from web import views
|
||||
|
||||
urlpatterns = [
|
||||
@@ -16,4 +17,13 @@ urlpatterns = [
|
||||
views.ActivityLogUpdateView.as_view(), name='activity_log_update'),
|
||||
path('users/<int:user_id>/activity_log/<int:activity_log_id>/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'),
|
||||
]
|
||||
|
20
web/views.py
20
web/views.py
@@ -1,7 +1,7 @@
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.views.generic import ListView, DetailView, CreateView, UpdateView, DeleteView
|
||||
from django.urls import reverse, reverse_lazy
|
||||
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):
|
||||
@@ -22,6 +22,7 @@ class UserDetailView(DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
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
|
||||
|
||||
|
||||
@@ -54,3 +55,16 @@ class ActivityLogDeleteView(DeleteView):
|
||||
return reverse('user', kwargs={
|
||||
'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
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user