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 @@
Confira os dados consolidados por tópico
| 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 }} | @@ -52,5 +54,61 @@{module.title} | ') + 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') + ))