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 %}
{% csrf_token %} {{ form.as_p }} - +
- - +{% 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 %} - - - - - - - - {% endfor %} -
+ Активность + + Когда начал + + Когда закончил + + +
- {{ activity_log.activity }} - - {{ activity_log.start_time }} - - {{ activity_log.end_time }} - -
{% csrf_token %} - -
-
-
{% csrf_token %} - -
-
-{% else %} -
  • Ничего не залогано :(
  • -{% endif %} + + {% for activity_log in activity_logs %} + + + {{ activity_log.activity }} + + + {{ activity_log.start_time }} + + + {{ activity_log.end_time }} + + + + +
    {% csrf_token %} + +
    + + +
    {% csrf_token %} + +
    + + + {% 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' %} -
    {% csrf_token %} - {{ form.as_p }} - -
    +{% block main %} - - +
    {% csrf_token %} + {{ form.as_p }} + +
    + +{% 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 + +