Skip to content

Commit fe9f85f

Browse files
renzonrenzon
authored andcommitted
Implemented Certificate List on Dashboard
close #1903
1 parent 41b886f commit fe9f85f

File tree

6 files changed

+320
-24
lines changed

6 files changed

+320
-24
lines changed

pythonpro/dashboard/facade.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,123 @@
1+
from functools import partial
2+
from itertools import chain, product
3+
from operator import attrgetter
4+
5+
import pytz
6+
from django.db.models import Count, Max, Min, Sum
7+
from django.utils.datetime_safe import datetime
8+
19
from pythonpro.dashboard.models import TopicInteraction as _TopicInteracion
10+
from pythonpro.modules.facade import get_entire_content_forest
11+
from pythonpro.modules.models import Topic
212

313

414
def has_watched_any_topic(user) -> bool:
515
return _TopicInteracion.objects.filter(user=user).exists()
16+
17+
18+
def calculate_topic_interaction_history(user):
19+
"""
20+
Calculate user's interaction history ordered by last topic interaction
21+
It will return a list of topics annotated with:
22+
1. last_interaction: last time user interacted with topic
23+
2. max_watched_time: max video time where user stopped watching
24+
3. total_watched_time: sum of all time user spent watching video
25+
4. interactions_count: number of times user started watching video
26+
5. duration: topic duration based on the min time this class had
27+
:param user:
28+
:return:
29+
"""
30+
31+
return Topic.objects.filter(
32+
topicinteraction__user=user
33+
).annotate(
34+
last_interaction=Max('topicinteraction__creation'),
35+
max_watched_time=Max('topicinteraction__max_watched_time'),
36+
total_watched_time=Sum('topicinteraction__total_watched_time'),
37+
interactions_count=Count('topicinteraction'),
38+
duration=Min('topicinteraction__topic_duration')
39+
).order_by('-last_interaction').select_related('chapter').select_related('chapter__section').select_related(
40+
'chapter__section__module')[:20]
41+
42+
43+
def calculate_module_progresses(user):
44+
"""
45+
Calculate the user progress on all modules
46+
:param user:
47+
:return:
48+
"""
49+
# arbitrary default value for last interaction
50+
default_min_time = datetime(1970, 1, 1, tzinfo=pytz.utc)
51+
property_defaults = {
52+
'last_interaction': default_min_time,
53+
'max_watched_time': 0,
54+
'total_watched_time': 0,
55+
'interactions_count': 0,
56+
'topics_count': 0,
57+
'finished_topics_count': 0,
58+
'duration': 0,
59+
}
60+
61+
sum_with_start_0 = partial(sum, start=0)
62+
63+
aggregation_functions = {
64+
'last_interaction': partial(max, default=default_min_time),
65+
'max_watched_time': sum_with_start_0,
66+
'total_watched_time': sum_with_start_0,
67+
'interactions_count': sum_with_start_0,
68+
'topics_count': sum_with_start_0,
69+
'finished_topics_count': sum_with_start_0,
70+
'duration': sum_with_start_0,
71+
}
72+
73+
def _aggregate_statistics(contents, content_children_property_name):
74+
for content, (property_, aggregation_function) in product(contents, aggregation_functions.items()):
75+
children = getattr(content, content_children_property_name)
76+
setattr(content, property_, aggregation_function(map(attrgetter(property_), children)))
77+
78+
def _flaten(iterable, children_property_name):
79+
for i in iterable:
80+
for child in getattr(i, children_property_name):
81+
yield child
82+
83+
def calculate_progression(content):
84+
try:
85+
return min(content.max_watched_time / content.duration, 1)
86+
except ZeroDivisionError:
87+
return 0
88+
89+
qs = Topic.objects.filter(topicinteraction__user=user).values('id').annotate(
90+
last_interaction=Max('topicinteraction__creation'),
91+
interactions_count=Max('topicinteraction'),
92+
max_watched_time=Max('topicinteraction__max_watched_time'),
93+
total_watched_time=Sum('topicinteraction__total_watched_time'),
94+
children_count=Count('topicinteraction'),
95+
duration=Min('topicinteraction__topic_duration')).all()
96+
97+
user_interacted_topics = {t['id']: t for t in qs}
98+
modules = get_entire_content_forest()
99+
all_sections = list(_flaten(modules, 'sections'))
100+
all_chapters = list(_flaten(all_sections, 'chapters'))
101+
all_topics = list(_flaten(all_chapters, 'topics'))
102+
for topic, (property_, default_value) in product(all_topics, property_defaults.items()):
103+
user_interaction_data = user_interacted_topics.get(topic.id, {})
104+
setattr(topic, property_, user_interaction_data.get(property_, default_value))
105+
for topic in all_topics:
106+
topic.progress = calculate_progression(topic)
107+
topic.topics_count = 1
108+
watched_to_end = topic.progress > 0.99
109+
spent_half_time_watching = topic.total_watched_time * 2 > topic.duration
110+
topic.finished_topics_count = 1 if (watched_to_end and spent_half_time_watching) else 0
111+
112+
contents_with_children_property_name = [
113+
(all_chapters, 'topics'),
114+
(all_sections, 'chapters'),
115+
(modules, 'sections')
116+
]
117+
for contents, content_children_property_name in contents_with_children_property_name:
118+
_aggregate_statistics(contents, content_children_property_name)
119+
120+
for content in chain(all_chapters, all_sections, modules):
121+
setattr(content, 'progress', calculate_progression(content))
122+
123+
return modules

pythonpro/dashboard/templates/dashboard/home.html

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@
1212
<div class="container mt-5 mb-5">
1313
<div class="row">
1414
<div class="col">
15-
<h1 class="mb-4">Dashboard</h1>
15+
<h1 class="mb-4">Dashboard</h1>
1616
<p>Confira os dados consolidados por tópico</p>
1717
<table class="table table-striped text-center">
1818
<thead>
1919
<tr>
20-
<th scope="col">Data</th>
20+
<th scope="col">Último Acesso</th>
2121
<th scope="col">Módulo</th>
2222
<th scope="col">Aula</th>
2323
<th scope="col" data-toggle="tooltip" data-placement="top"
@@ -35,7 +35,9 @@ <h1 class="mb-4">Dashboard</h1>
3535
{% for topic in topics %}
3636
<tr>
3737
<td>{{ topic.last_interaction|date:"d/m/Y" }} {{ topic.last_interaction|time:"H:i:s" }}</td>
38-
<td><a href="{{topic.calculated_module.get_absolute_url}}">{{topic.calculated_module.title}}</a></td>
38+
<td>
39+
<a href="{{ topic.calculated_module.get_absolute_url }}">{{ topic.calculated_module.title }}</a>
40+
</td>
3941
<td><a href="{{ topic.get_absolute_url }}">{{ topic.title }}</a></td>
4042
<td>{{ topic.max_watched_time|duration }}</td>
4143
<td>{{ topic.total_watched_time|duration }}</td>
@@ -52,5 +54,61 @@ <h1 class="mb-4">Dashboard</h1>
5254
</div>
5355
</div>
5456

57+
<div class="row">
58+
<div class="col">
59+
<h2 class="mb-4">Progresso por Módulo</h2>
60+
<div class="row">
61+
{% for module_progress in module_progresses %}
62+
<div class="col-12 col-lg-6 mb-md-4 mb-4">
63+
<div class="card">
64+
<div class="card-header bg-primary text-light">
65+
Módulo: {{ module_progress.title }}
66+
</div>
67+
<div class="card-body">
68+
69+
<dl>
70+
<dt>Aulas <span data-toggle="tooltip" data-placement="top"
71+
title="Quantidade Total de Aulas no Módulo"
72+
class="badge badge-pill badge-dark">?</span></dt>
73+
<dd>{{ module_progress.topics_count }}</dd>
74+
<dt>Aulas Finalizadas <span data-toggle="tooltip" data-placement="top"
75+
title="Quantidade de Aulas que você viu até o fim"
76+
class="badge badge-pill badge-dark">?</span></dt>
77+
<dd>{{ module_progress.finished_topics_count }}</dd>
78+
79+
<dt>
80+
Carga Horária <span data-toggle="tooltip" data-placement="top"
81+
title="Somatório das durações de todas aulas em hora, minuto e segundo"
82+
class="badge badge-pill badge-dark">?</span></dt>
83+
<dd>{{ module_progress.duration|duration }}</dd>
84+
<dt>Total Assistido <span data-toggle="tooltip" data-placement="top"
85+
title="Somatório do tempo que você assistou aulas"
86+
class="badge badge-pill badge-dark">?</span></dt>
87+
<dd>{{ module_progress.total_watched_time|duration }}</dd>
88+
<dt>Último Acesso</dt>
89+
<dd>{% if module_progress.interactions_count %}
90+
{{ module_progress.last_interaction|date:"d/m/Y" }}
91+
{% else %}
92+
--
93+
{% endif %}
94+
</dd>
95+
</dl>
96+
<div class="progress">
97+
<div class="progress-bar bg-success" role="progressbar"
98+
style="width: {% widthratio module_progress.progress 1 100 %}%"
99+
aria-valuenow="{% widthratio module_progress.progress 1 100 %}"
100+
aria-valuemin="0"
101+
aria-valuemax="100">{% if module_progress.progress > 0.09 %}
102+
{% widthratio module_progress.progress 1 100 %}%{% endif %}</div>
103+
</div>
104+
</div>
105+
</div>
106+
</div>
107+
{% endfor %}
108+
</div>
109+
</div>
110+
</div>
111+
112+
55113
</div>
56114
{% endblock body %}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import pytest
2+
from django.urls import reverse
3+
from model_mommy import mommy
4+
5+
from pythonpro.dashboard import facade
6+
from pythonpro.dashboard.models import TopicInteraction
7+
from pythonpro.django_assertions import dj_assert_contains
8+
from pythonpro.modules.models import Chapter, Module, Section, Topic
9+
10+
11+
# @pytest.fixture
12+
# def interactions(logged_user, topic):
13+
# with freeze_time("2019-07-22 00:00:00"):
14+
# first_interaction = mommy.make(
15+
# TopicInteraction,
16+
# user=logged_user,
17+
# topic=topic,
18+
# topic_duration=125,
19+
# total_watched_time=125,
20+
# max_watched_time=95
21+
# )
22+
#
23+
# with freeze_time("2019-07-22 01:00:00"):
24+
# second_interaction = mommy.make(
25+
# TopicInteraction,
26+
# user=logged_user,
27+
# topic=topic,
28+
# topic_duration=125,
29+
# total_watched_time=34,
30+
# max_watched_time=14
31+
# )
32+
# with freeze_time("2019-07-22 00:30:00"):
33+
# third_interaction = mommy.make(
34+
# TopicInteraction,
35+
# user=logged_user,
36+
# topic=topic,
37+
# topic_duration=125,
38+
# total_watched_time=64,
39+
# max_watched_time=34
40+
# )
41+
# return [
42+
# first_interaction,
43+
# second_interaction,
44+
# third_interaction,
45+
# ]
46+
47+
48+
@pytest.fixture
49+
def modules(db):
50+
return mommy.make(Module, 2)
51+
52+
53+
@pytest.fixture
54+
def sections(modules):
55+
models = []
56+
for m in modules:
57+
models.extend(mommy.make(Section, 2, module=m))
58+
return models
59+
60+
61+
@pytest.fixture
62+
def chapters(sections):
63+
models = []
64+
for s in sections:
65+
models.extend(mommy.make(Chapter, 2, section=s))
66+
return models
67+
68+
69+
@pytest.fixture
70+
def topics(chapters):
71+
models = []
72+
for c in chapters:
73+
models.extend(mommy.make(Topic, 2, chapter=c))
74+
return models
75+
76+
77+
@pytest.fixture
78+
def interactions(topics, logged_user):
79+
models = []
80+
for t in topics:
81+
models.append(
82+
mommy.make(
83+
TopicInteraction,
84+
user=logged_user,
85+
topic=t,
86+
topic_duration=100,
87+
total_watched_time=100,
88+
max_watched_time=50
89+
)
90+
)
91+
return models
92+
93+
94+
@pytest.fixture
95+
def resp(client_with_lead, interactions):
96+
return client_with_lead.get(
97+
reverse('dashboard:home'),
98+
secure=True
99+
)
100+
101+
102+
def test_module_title_is_present_on_card(resp, modules):
103+
for m in modules:
104+
dj_assert_contains(resp, f'Módulo: {m.title}')
105+
106+
107+
def test_module_percentage_style_on_card(resp, logged_user):
108+
for m in facade.calculate_module_progresses(logged_user):
109+
dj_assert_contains(resp, f'style="width: {m.progress:.0%}"')

pythonpro/dashboard/tests/test_dashboard/test_topic_interaction_aggregate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_topic_url(resp, topic: Topic):
7777

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

8282

8383
def test_max_creation(resp, interactions):

pythonpro/dashboard/views.py

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,27 @@
11
from django.contrib.auth.decorators import login_required
2-
from django.db.models import Count, Max, Sum
32
from django.http import JsonResponse
43
from django.shortcuts import render
54
from django.views.decorators.csrf import csrf_exempt
65

6+
from pythonpro.dashboard import facade as dashboard_facade
77
from pythonpro.dashboard.facade import has_watched_any_topic
88
from pythonpro.dashboard.forms import TopicInteractionForm
99
from pythonpro.domain import user_facade
10-
from pythonpro.modules.models import Topic
1110

1211

1312
@login_required
1413
def home(request):
15-
topics = list(
16-
Topic.objects.filter(
17-
topicinteraction__user=request.user
18-
).annotate(
19-
last_interaction=Max('topicinteraction__creation')
20-
).annotate(
21-
max_watched_time=Max('topicinteraction__max_watched_time')
22-
).annotate(
23-
total_watched_time=Sum('topicinteraction__total_watched_time')
24-
).annotate(
25-
interactions_count=Count('topicinteraction')
26-
).order_by('-last_interaction').select_related('chapter').select_related('chapter__section').select_related(
27-
'chapter__section__module')[:20]
28-
)
14+
topics = list(dashboard_facade.calculate_topic_interaction_history(request.user))
2915

3016
for topic in topics:
3117
topic.calculated_module = topic.find_module()
3218

19+
module_progresses = dashboard_facade.calculate_module_progresses(request.user)
20+
ctx = {'topics': topics, 'module_progresses': module_progresses}
3321
return render(
3422
request,
3523
'dashboard/home.html',
36-
{
37-
'topics': topics,
38-
}
24+
ctx
3925
)
4026

4127

0 commit comments

Comments
 (0)