diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e68e06a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+*.sqlite3
+
+__pycache__
diff --git a/db.sqlite3 b/db.sqlite3
deleted file mode 100644
index db42473..0000000
Binary files a/db.sqlite3 and /dev/null differ
diff --git a/templates/web/activitylog_form.html b/templates/web/activitylog_form.html
index 02966b3..f7089bd 100644
--- a/templates/web/activitylog_form.html
+++ b/templates/web/activitylog_form.html
@@ -1,15 +1,10 @@
-
-
-
-
- Новая запись
-
-
+{% extends 'web/base.html' %}
+
+{% block main %}
-
-
+{% endblock %}
diff --git a/templates/web/base.html b/templates/web/base.html
new file mode 100644
index 0000000..a5cd6c1
--- /dev/null
+++ b/templates/web/base.html
@@ -0,0 +1,39 @@
+{% load static %}
+
+
+
+
+
+
+ {% block title %}
+ LifeStats
+ {% endblock %}
+
+
+
+
+
+
+
+
+ {% block head %}
+ {% endblock %}
+
+
+{% block body %}
+
+ {% block main %}
+ {% endblock %}
+
+{% endblock %}
+
+
diff --git a/templates/web/user_activity_charts.html b/templates/web/user_activity_charts.html
new file mode 100644
index 0000000..1783457
--- /dev/null
+++ b/templates/web/user_activity_charts.html
@@ -0,0 +1,28 @@
+{% extends 'web/base.html' %}
+
+{% block main %}
+
+
+
+
+ {{ user }} — {{ activity }}
+
+
+
+
+ {#
#}
+
+{% endblock %}
diff --git a/templates/web/user_detail.html b/templates/web/user_detail.html
index 38a3164..7595880 100644
--- a/templates/web/user_detail.html
+++ b/templates/web/user_detail.html
@@ -1,71 +1,100 @@
-
-
-
-
- Пользователь
-
-
+{% extends 'web/base.html' %}
-{{ user }}
+{% block main %}
+
+
+
+ {{ user }}
-Записи
+
+
+
Записи
-
Создать новую запись
+
+ Создать новую запись
+
-{% if activity_logs %}
-
-
-
- Активность
- |
-
- Когда начал
- |
-
- Когда закончил
- |
-
- |
-
- |
-
+ {% if activity_logs %}
+
+
+
+
+ Активность
+ |
+
+ Когда начал
+ |
+
+ Когда закончил
+ |
+
+ |
+
+ |
+
+
- {% for activity_log in activity_logs %}
-
-
- {{ activity_log.activity }}
- |
-
- {{ activity_log.start_time }}
- |
-
- {{ activity_log.end_time }}
- |
-
-
- |
-
-
- |
-
- {% endfor %}
-
-{% else %}
- Ничего не залогано :(
-{% endif %}
+
+ {% for activity_log in activity_logs %}
+
+
+ {{ activity_log.activity }}
+ |
+
+ {{ activity_log.start_time }}
+ |
+
+ {{ activity_log.end_time }}
+ |
+
+
+
+
+ |
+
+
+ |
+
+ {% endfor %}
+
+
+ {% else %}
+
Ничего не залогано :(
+ {% endif %}
+
+
+
+

+
+
Активности
+
+
+
-
-
-
-
-
+{% endblock %}
diff --git a/templates/web/user_form.html b/templates/web/user_form.html
index f378453..294e41e 100644
--- a/templates/web/user_form.html
+++ b/templates/web/user_form.html
@@ -1,15 +1,10 @@
-
-
-
-
- Новый пользователь
-
-
+{% extends 'web/base.html' %}
-
+{% block main %}
-
-
+
+
+{% endblock %}
diff --git a/templates/web/user_list.html b/templates/web/user_list.html
index 50de522..cb55bb8 100644
--- a/templates/web/user_list.html
+++ b/templates/web/user_list.html
@@ -1,28 +1,23 @@
-
-
-
-
- Пользователи
-
-
+{% extends 'web/base.html' %}
-Пользователи
-
+{% block main %}
+
+ Пользователи
+
-
- Создать нового пользователя
-
+
+ Создать нового пользователя
+
-
-
+{% endblock %}
diff --git a/timelogger/__pycache__/__init__.cpython-37.pyc b/timelogger/__pycache__/__init__.cpython-37.pyc
deleted file mode 100644
index 4f7bc51..0000000
Binary files a/timelogger/__pycache__/__init__.cpython-37.pyc and /dev/null differ
diff --git a/timelogger/__pycache__/settings.cpython-37.pyc b/timelogger/__pycache__/settings.cpython-37.pyc
deleted file mode 100644
index c991864..0000000
Binary files a/timelogger/__pycache__/settings.cpython-37.pyc and /dev/null differ
diff --git a/timelogger/__pycache__/urls.cpython-37.pyc b/timelogger/__pycache__/urls.cpython-37.pyc
deleted file mode 100644
index 2183ad4..0000000
Binary files a/timelogger/__pycache__/urls.cpython-37.pyc and /dev/null differ
diff --git a/timelogger/__pycache__/wsgi.cpython-37.pyc b/timelogger/__pycache__/wsgi.cpython-37.pyc
deleted file mode 100644
index 8a9731c..0000000
Binary files a/timelogger/__pycache__/wsgi.cpython-37.pyc and /dev/null differ
diff --git a/timelogger/settings.py b/timelogger/settings.py
index cb9d80c..66f7872 100644
--- a/timelogger/settings.py
+++ b/timelogger/settings.py
@@ -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 = ''
diff --git a/web/__pycache__/__init__.cpython-37.pyc b/web/__pycache__/__init__.cpython-37.pyc
deleted file mode 100644
index a120302..0000000
Binary files a/web/__pycache__/__init__.cpython-37.pyc and /dev/null differ
diff --git a/web/__pycache__/apps.cpython-37.pyc b/web/__pycache__/apps.cpython-37.pyc
deleted file mode 100644
index 1a18034..0000000
Binary files a/web/__pycache__/apps.cpython-37.pyc and /dev/null differ
diff --git a/web/__pycache__/models.cpython-37.pyc b/web/__pycache__/models.cpython-37.pyc
deleted file mode 100644
index 8cc4767..0000000
Binary files a/web/__pycache__/models.cpython-37.pyc and /dev/null differ
diff --git a/web/__pycache__/urls.cpython-37.pyc b/web/__pycache__/urls.cpython-37.pyc
deleted file mode 100644
index 9fca362..0000000
Binary files a/web/__pycache__/urls.cpython-37.pyc and /dev/null differ
diff --git a/web/__pycache__/views.cpython-37.pyc b/web/__pycache__/views.cpython-37.pyc
deleted file mode 100644
index afebbf5..0000000
Binary files a/web/__pycache__/views.cpython-37.pyc and /dev/null differ
diff --git a/web/chart_views.py b/web/chart_views.py
new file mode 100644
index 0000000..55066a0
--- /dev/null
+++ b/web/chart_views.py
@@ -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')
diff --git a/web/migrations/__pycache__/0001_initial.cpython-37.pyc b/web/migrations/__pycache__/0001_initial.cpython-37.pyc
deleted file mode 100644
index 6caea0c..0000000
Binary files a/web/migrations/__pycache__/0001_initial.cpython-37.pyc and /dev/null differ
diff --git a/web/migrations/__pycache__/__init__.cpython-37.pyc b/web/migrations/__pycache__/__init__.cpython-37.pyc
deleted file mode 100644
index a84e178..0000000
Binary files a/web/migrations/__pycache__/__init__.cpython-37.pyc and /dev/null differ
diff --git a/web/models.py b/web/models.py
index 01bea2d..91a2fbc 100644
--- a/web/models.py
+++ b/web/models.py
@@ -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):
diff --git a/web/urls.py b/web/urls.py
index 5248f10..a4bba5e 100644
--- a/web/urls.py
+++ b/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//activity_log//delete',
views.ActivityLogDeleteView.as_view(), name='activity_log_delete'),
+
+ path('users//charts/pie',
+ chart_views.UserChartsPie.as_view(), name='user_charts_pie'),
+ path('users//charts/activity/',
+ views.UserChartsActivity.as_view(), name='user_charts_activity'),
+ path('users//charts/activity//all',
+ chart_views.UserChartsActivityAll.as_view(), name='user_charts_activity_all'),
+ # path('users//charts/activity//tracker',
+ # chart_views.UserActivityChartsTracker.as_view(), name='user_charts_activity_tracker'),
]
diff --git a/web/views.py b/web/views.py
index f6e5f33..0ddd541 100644
--- a/web/views.py
+++ b/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
+
+