Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions pythonpro/dashboard/facade.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,123 @@
from functools import partial
from itertools import chain, product
from operator import attrgetter

import pytz
from django.db.models import Count, Max, Min, Sum
from django.utils.datetime_safe import datetime

from pythonpro.dashboard.models import TopicInteraction as _TopicInteracion
from pythonpro.modules.facade import get_entire_content_forest
from pythonpro.modules.models import Topic


def has_watched_any_topic(user) -> bool:
return _TopicInteracion.objects.filter(user=user).exists()


def calculate_topic_interaction_history(user):
"""
Calculate user's interaction history ordered by last topic interaction
It will return a list of topics annotated with:
1. last_interaction: last time user interacted with topic
2. max_watched_time: max video time where user stopped watching
3. total_watched_time: sum of all time user spent watching video
4. interactions_count: number of times user started watching video
5. duration: topic duration based on the min time this class had
:param user:
:return:
"""

return Topic.objects.filter(
topicinteraction__user=user
).annotate(
last_interaction=Max('topicinteraction__creation'),
max_watched_time=Max('topicinteraction__max_watched_time'),
total_watched_time=Sum('topicinteraction__total_watched_time'),
interactions_count=Count('topicinteraction'),
duration=Min('topicinteraction__topic_duration')
).order_by('-last_interaction').select_related('chapter').select_related('chapter__section').select_related(
'chapter__section__module')[:20]


def calculate_module_progresses(user):
"""
Calculate the user progress on all modules
:param user:
:return:
"""
# arbitrary default value for last interaction
default_min_time = datetime(1970, 1, 1, tzinfo=pytz.utc)
property_defaults = {
'last_interaction': default_min_time,
'max_watched_time': 0,
'total_watched_time': 0,
'interactions_count': 0,
'topics_count': 0,
'finished_topics_count': 0,
'duration': 0,
}

sum_with_start_0 = partial(sum, start=0)

aggregation_functions = {
'last_interaction': partial(max, default=default_min_time),
'max_watched_time': sum_with_start_0,
'total_watched_time': sum_with_start_0,
'interactions_count': sum_with_start_0,
'topics_count': sum_with_start_0,
'finished_topics_count': sum_with_start_0,
'duration': sum_with_start_0,
}

def _aggregate_statistics(contents, content_children_property_name):
for content, (property_, aggregation_function) in product(contents, aggregation_functions.items()):
children = getattr(content, content_children_property_name)
setattr(content, property_, aggregation_function(map(attrgetter(property_), children)))

def _flaten(iterable, children_property_name):
for i in iterable:
for child in getattr(i, children_property_name):
yield child

def calculate_progression(content):
try:
return min(content.max_watched_time / content.duration, 1)
except ZeroDivisionError:
return 0

qs = Topic.objects.filter(topicinteraction__user=user).values('id').annotate(
last_interaction=Max('topicinteraction__creation'),
interactions_count=Max('topicinteraction'),
max_watched_time=Max('topicinteraction__max_watched_time'),
total_watched_time=Sum('topicinteraction__total_watched_time'),
children_count=Count('topicinteraction'),
duration=Min('topicinteraction__topic_duration')).all()

user_interacted_topics = {t['id']: t for t in qs}
modules = get_entire_content_forest()
all_sections = list(_flaten(modules, 'sections'))
all_chapters = list(_flaten(all_sections, 'chapters'))
all_topics = list(_flaten(all_chapters, 'topics'))
for topic, (property_, default_value) in product(all_topics, property_defaults.items()):
user_interaction_data = user_interacted_topics.get(topic.id, {})
setattr(topic, property_, user_interaction_data.get(property_, default_value))
for topic in all_topics:
topic.progress = calculate_progression(topic)
topic.topics_count = 1
watched_to_end = topic.progress > 0.99
spent_half_time_watching = topic.total_watched_time * 2 > topic.duration
topic.finished_topics_count = 1 if (watched_to_end and spent_half_time_watching) else 0

contents_with_children_property_name = [
(all_chapters, 'topics'),
(all_sections, 'chapters'),
(modules, 'sections')
]
for contents, content_children_property_name in contents_with_children_property_name:
_aggregate_statistics(contents, content_children_property_name)

for content in chain(all_chapters, all_sections, modules):
setattr(content, 'progress', calculate_progression(content))

return modules
64 changes: 61 additions & 3 deletions pythonpro/dashboard/templates/dashboard/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@
<div class="container mt-5 mb-5">
<div class="row">
<div class="col">
<h1 class="mb-4">Dashboard</h1>
<h1 class="mb-4">Dashboard</h1>
<p>Confira os dados consolidados por tópico</p>
<table class="table table-striped text-center">
<thead>
<tr>
<th scope="col">Data</th>
<th scope="col">Último Acesso</th>
<th scope="col">Módulo</th>
<th scope="col">Aula</th>
<th scope="col" data-toggle="tooltip" data-placement="top"
Expand All @@ -35,7 +35,9 @@ <h1 class="mb-4">Dashboard</h1>
{% for topic in topics %}
<tr>
<td>{{ topic.last_interaction|date:"d/m/Y" }} {{ topic.last_interaction|time:"H:i:s" }}</td>
<td><a href="{{topic.calculated_module.get_absolute_url}}">{{topic.calculated_module.title}}</a></td>
<td>
<a href="{{ topic.calculated_module.get_absolute_url }}">{{ topic.calculated_module.title }}</a>
</td>
<td><a href="{{ topic.get_absolute_url }}">{{ topic.title }}</a></td>
<td>{{ topic.max_watched_time|duration }}</td>
<td>{{ topic.total_watched_time|duration }}</td>
Expand All @@ -52,5 +54,61 @@ <h1 class="mb-4">Dashboard</h1>
</div>
</div>

<div class="row">
<div class="col">
<h2 class="mb-4">Progresso por Módulo</h2>
<div class="row">
{% for module_progress in module_progresses %}
<div class="col-12 col-lg-6 mb-md-4 mb-4">
<div class="card">
<div class="card-header bg-primary text-light">
Módulo: {{ module_progress.title }}
</div>
<div class="card-body">

<dl>
<dt>Aulas <span data-toggle="tooltip" data-placement="top"
title="Quantidade Total de Aulas no Módulo"
class="badge badge-pill badge-dark">?</span></dt>
<dd>{{ module_progress.topics_count }}</dd>
<dt>Aulas Finalizadas <span data-toggle="tooltip" data-placement="top"
title="Quantidade de Aulas que você viu até o fim"
class="badge badge-pill badge-dark">?</span></dt>
<dd>{{ module_progress.finished_topics_count }}</dd>

<dt>
Carga Horária <span data-toggle="tooltip" data-placement="top"
title="Somatório das durações de todas aulas em hora, minuto e segundo"
class="badge badge-pill badge-dark">?</span></dt>
<dd>{{ module_progress.duration|duration }}</dd>
<dt>Total Assistido <span data-toggle="tooltip" data-placement="top"
title="Somatório do tempo que você assistou aulas"
class="badge badge-pill badge-dark">?</span></dt>
<dd>{{ module_progress.total_watched_time|duration }}</dd>
<dt>Último Acesso</dt>
<dd>{% if module_progress.interactions_count %}
{{ module_progress.last_interaction|date:"d/m/Y" }}
{% else %}
--
{% endif %}
</dd>
</dl>
<div class="progress">
<div class="progress-bar bg-success" role="progressbar"
style="width: {% widthratio module_progress.progress 1 100 %}%"
aria-valuenow="{% widthratio module_progress.progress 1 100 %}"
aria-valuemin="0"
aria-valuemax="100">{% if module_progress.progress > 0.09 %}
{% widthratio module_progress.progress 1 100 %}%{% endif %}</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>


</div>
{% endblock body %}
109 changes: 109 additions & 0 deletions pythonpro/dashboard/tests/test_dashboard/test_certificate_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import pytest
from django.urls import reverse
from model_mommy import mommy

from pythonpro.dashboard import facade
from pythonpro.dashboard.models import TopicInteraction
from pythonpro.django_assertions import dj_assert_contains
from pythonpro.modules.models import Chapter, Module, Section, Topic


# @pytest.fixture
# def interactions(logged_user, topic):
# with freeze_time("2019-07-22 00:00:00"):
# first_interaction = mommy.make(
# TopicInteraction,
# user=logged_user,
# topic=topic,
# topic_duration=125,
# total_watched_time=125,
# max_watched_time=95
# )
#
# with freeze_time("2019-07-22 01:00:00"):
# second_interaction = mommy.make(
# TopicInteraction,
# user=logged_user,
# topic=topic,
# topic_duration=125,
# total_watched_time=34,
# max_watched_time=14
# )
# with freeze_time("2019-07-22 00:30:00"):
# third_interaction = mommy.make(
# TopicInteraction,
# user=logged_user,
# topic=topic,
# topic_duration=125,
# total_watched_time=64,
# max_watched_time=34
# )
# return [
# first_interaction,
# second_interaction,
# third_interaction,
# ]


@pytest.fixture
def modules(db):
return mommy.make(Module, 2)


@pytest.fixture
def sections(modules):
models = []
for m in modules:
models.extend(mommy.make(Section, 2, module=m))
return models


@pytest.fixture
def chapters(sections):
models = []
for s in sections:
models.extend(mommy.make(Chapter, 2, section=s))
return models


@pytest.fixture
def topics(chapters):
models = []
for c in chapters:
models.extend(mommy.make(Topic, 2, chapter=c))
return models


@pytest.fixture
def interactions(topics, logged_user):
models = []
for t in topics:
models.append(
mommy.make(
TopicInteraction,
user=logged_user,
topic=t,
topic_duration=100,
total_watched_time=100,
max_watched_time=50
)
)
return models


@pytest.fixture
def resp(client_with_lead, interactions):
return client_with_lead.get(
reverse('dashboard:home'),
secure=True
)


def test_module_title_is_present_on_card(resp, modules):
for m in modules:
dj_assert_contains(resp, f'Módulo: {m.title}')


def test_module_percentage_style_on_card(resp, logged_user):
for m in facade.calculate_module_progresses(logged_user):
dj_assert_contains(resp, f'style="width: {m.progress:.0%}"')
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def test_topic_url(resp, topic: Topic):

def test_module_table_row(resp, topic: Topic):
module = topic.find_module()
dj_assert_contains(resp, f'<td><a href="{module.get_absolute_url()}">{module.title}</a></td>')
dj_assert_contains(resp, f'<a href="{module.get_absolute_url()}">{module.title}</a>')


def test_max_creation(resp, interactions):
Expand Down
24 changes: 5 additions & 19 deletions pythonpro/dashboard/views.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
from django.contrib.auth.decorators import login_required
from django.db.models import Count, Max, Sum
from django.http import JsonResponse
from django.shortcuts import render
from django.views.decorators.csrf import csrf_exempt

from pythonpro.dashboard import facade as dashboard_facade
from pythonpro.dashboard.facade import has_watched_any_topic
from pythonpro.dashboard.forms import TopicInteractionForm
from pythonpro.domain import user_facade
from pythonpro.modules.models import Topic


@login_required
def home(request):
topics = list(
Topic.objects.filter(
topicinteraction__user=request.user
).annotate(
last_interaction=Max('topicinteraction__creation')
).annotate(
max_watched_time=Max('topicinteraction__max_watched_time')
).annotate(
total_watched_time=Sum('topicinteraction__total_watched_time')
).annotate(
interactions_count=Count('topicinteraction')
).order_by('-last_interaction').select_related('chapter').select_related('chapter__section').select_related(
'chapter__section__module')[:20]
)
topics = list(dashboard_facade.calculate_topic_interaction_history(request.user))

for topic in topics:
topic.calculated_module = topic.find_module()

module_progresses = dashboard_facade.calculate_module_progresses(request.user)
ctx = {'topics': topics, 'module_progresses': module_progresses}
return render(
request,
'dashboard/home.html',
{
'topics': topics,
}
ctx
)


Expand Down
Loading