diff --git a/pythonpro/dashboard/facade.py b/pythonpro/dashboard/facade.py index 08f87cbb..ba1079c8 100644 --- a/pythonpro/dashboard/facade.py +++ b/pythonpro/dashboard/facade.py @@ -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 diff --git a/pythonpro/dashboard/templates/dashboard/home.html b/pythonpro/dashboard/templates/dashboard/home.html index 68ad8a6f..6ebdbc26 100644 --- a/pythonpro/dashboard/templates/dashboard/home.html +++ b/pythonpro/dashboard/templates/dashboard/home.html @@ -12,12 +12,12 @@
-

Dashboard

+

Dashboard

Confira os dados consolidados por tópico

- + - + @@ -52,5 +54,61 @@

Dashboard

+
+
+

Progresso por Módulo

+
+ {% for module_progress in module_progresses %} +
+
+
+ Módulo: {{ module_progress.title }} +
+
+ +
+
Aulas ?
+
{{ module_progress.topics_count }}
+
Aulas Finalizadas ?
+
{{ module_progress.finished_topics_count }}
+ +
+ Carga Horária ?
+
{{ module_progress.duration|duration }}
+
Total Assistido ?
+
{{ module_progress.total_watched_time|duration }}
+
Último Acesso
+
{% if module_progress.interactions_count %} + {{ module_progress.last_interaction|date:"d/m/Y" }} + {% else %} + -- + {% endif %} +
+
+
+
{% if module_progress.progress > 0.09 %} + {% widthratio module_progress.progress 1 100 %}%{% endif %}
+
+
+
+
+ {% endfor %} +
+
+
+ + {% endblock body %} \ No newline at end of file diff --git a/pythonpro/dashboard/tests/test_dashboard/test_certificate_list.py b/pythonpro/dashboard/tests/test_dashboard/test_certificate_list.py new file mode 100644 index 00000000..a4a2eb14 --- /dev/null +++ b/pythonpro/dashboard/tests/test_dashboard/test_certificate_list.py @@ -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%}"') diff --git a/pythonpro/dashboard/tests/test_dashboard/test_topic_interaction_aggregate.py b/pythonpro/dashboard/tests/test_dashboard/test_topic_interaction_aggregate.py index cd693c3a..53300ca3 100644 --- a/pythonpro/dashboard/tests/test_dashboard/test_topic_interaction_aggregate.py +++ b/pythonpro/dashboard/tests/test_dashboard/test_topic_interaction_aggregate.py @@ -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'') + dj_assert_contains(resp, f'{module.title}') def test_max_creation(resp, interactions): diff --git a/pythonpro/dashboard/views.py b/pythonpro/dashboard/views.py index c4931254..753925c8 100644 --- a/pythonpro/dashboard/views.py +++ b/pythonpro/dashboard/views.py @@ -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 ) diff --git a/pythonpro/modules/facade.py b/pythonpro/modules/facade.py index 7f8569bb..734194cc 100644 --- a/pythonpro/modules/facade.py +++ b/pythonpro/modules/facade.py @@ -1,6 +1,6 @@ from django.db.models import Prefetch -from pythonpro.modules.models import (Section as _Section, Module as _Module, Chapter as _Chapter, Topic as _Topic) +from pythonpro.modules.models import (Chapter as _Chapter, Module as _Module, Section as _Section, Topic as _Topic) def get_topic_model(): @@ -74,3 +74,28 @@ def get_topic_with_contents(slug): """ return _Topic.objects.filter(slug=slug).select_related('chapter').select_related('chapter__section').select_related( 'chapter__section__module').get() + + +def get_entire_content_forest(): + """ + Return a list of modules with the entire content on it + :return: + """ + return list(_Module.objects.all().prefetch_related( + Prefetch( + 'section_set', + queryset=_Section.objects.order_by('order').prefetch_related( + Prefetch( + 'chapter_set', + queryset=_Chapter.objects.order_by('order').prefetch_related( + Prefetch( + 'topic_set', + queryset=_Topic.objects.order_by('order'), + to_attr='topics' + ) + ), + to_attr='chapters' + ) + ), + to_attr='sections') + ))
DataÚltimo Acesso Módulo Aula Dashboard {% for topic in topics %}
{{ topic.last_interaction|date:"d/m/Y" }} {{ topic.last_interaction|time:"H:i:s" }}{{topic.calculated_module.title}} + {{ topic.calculated_module.title }} + {{ topic.title }} {{ topic.max_watched_time|duration }} {{ topic.total_watched_time|duration }} {module.title}