From fe5b3c41aa898fa7491a7ec9bce28c5e1c5b542d Mon Sep 17 00:00:00 2001
From: adityacp
Date: Fri, 6 Nov 2020 18:21:48 +0530
Subject: Statistics app for video tracking
---
stats/__init__.py | 0
stats/admin.py | 8 ++++
stats/apps.py | 5 +++
stats/models.py | 24 +++++++++++
stats/templates/view_lesson_tracking.html | 69 +++++++++++++++++++++++++++++++
stats/tests.py | 3 ++
stats/urls.py | 12 ++++++
stats/views.py | 54 ++++++++++++++++++++++++
8 files changed, 175 insertions(+)
create mode 100644 stats/__init__.py
create mode 100644 stats/admin.py
create mode 100644 stats/apps.py
create mode 100644 stats/models.py
create mode 100644 stats/templates/view_lesson_tracking.html
create mode 100644 stats/tests.py
create mode 100644 stats/urls.py
create mode 100644 stats/views.py
(limited to 'stats')
diff --git a/stats/__init__.py b/stats/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/stats/admin.py b/stats/admin.py
new file mode 100644
index 0000000..b400d27
--- /dev/null
+++ b/stats/admin.py
@@ -0,0 +1,8 @@
+# Django Imports
+from django.contrib import admin
+
+# Local Imports
+from stats.models import TrackLesson
+
+
+admin.site.register(TrackLesson)
diff --git a/stats/apps.py b/stats/apps.py
new file mode 100644
index 0000000..2d09b92
--- /dev/null
+++ b/stats/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class StatsConfig(AppConfig):
+ name = 'stats'
diff --git a/stats/models.py b/stats/models.py
new file mode 100644
index 0000000..f2f1bce
--- /dev/null
+++ b/stats/models.py
@@ -0,0 +1,24 @@
+# Django Imports
+from django.db import models
+from django.utils import timezone
+from django.contrib.auth.models import User
+
+# Local Imports
+from yaksh.models import Course, Lesson
+
+
+class TrackLesson(models.Model):
+ user = models.ForeignKey(User, on_delete=models.CASCADE)
+ course = models.ForeignKey(Course, on_delete=models.CASCADE)
+ lesson = models.ForeignKey(Lesson, on_delete=models.CASCADE)
+ current_time = models.CharField(max_length=100, default="00:00:00")
+ video_duration = models.CharField(max_length=100, default="00:00:00")
+ last_access_time = models.DateTimeField(default=timezone.now)
+ creation_time = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ unique_together = ('user', 'course', 'lesson')
+
+ def __str__(self):
+ return (f"Track {self.lesson} in {self.course} "
+ f"for {self.user.get_full_name()}")
diff --git a/stats/templates/view_lesson_tracking.html b/stats/templates/view_lesson_tracking.html
new file mode 100644
index 0000000..fd87d70
--- /dev/null
+++ b/stats/templates/view_lesson_tracking.html
@@ -0,0 +1,69 @@
+{% extends "manage.html" %}
+
+{% block title %} Lesson Views {% endblock %}
+{% block script %}
+
+{% endblock %}
+{% block content %}
+
+ {% with objects.object_list as trackings %}
+
+ Statistics for {% with trackings|first as entry %} {{entry.lesson}} {% endwith %}
+
+
+ Back
+
+
+ {% include "yaksh/paginator.html" %}
+
+
{{total}} student(s) viewed this lesson
+
+ {% endwith %}
+
+ {% include "yaksh/paginator.html" %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/stats/tests.py b/stats/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/stats/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/stats/urls.py b/stats/urls.py
new file mode 100644
index 0000000..f11148f
--- /dev/null
+++ b/stats/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+from stats import views
+
+
+app_name = "stats"
+
+urlpatterns = [
+ path('submit/video/watch/',
+ views.add_tracker, name='add_tracker'),
+ path('view/watch/stats//',
+ views.view_lesson_watch_stats, name='view_lesson_watch_stats'),
+]
diff --git a/stats/views.py b/stats/views.py
new file mode 100644
index 0000000..ddbc1b3
--- /dev/null
+++ b/stats/views.py
@@ -0,0 +1,54 @@
+# Django Imports
+from django.shortcuts import render, get_object_or_404
+from django.http import JsonResponse
+from django.utils import timezone
+from django.contrib.auth.decorators import login_required
+from django.core.paginator import Paginator
+
+# Local Imports
+from stats.models import TrackLesson
+from yaksh.models import Course
+from yaksh.decorators import email_verified
+
+
+@login_required
+@email_verified
+def add_tracker(request, tracker_id):
+ user = request.user
+ track = get_object_or_404(
+ TrackLesson.objects.select_related("course"), id=tracker_id
+ )
+ if not track.course.is_student(user):
+ raise Http404("You are not enrolled in this course")
+ context = {}
+ video_duration = request.POST.get('video_duration')
+ current_time = request.POST.get('current_video_time')
+ if current_time:
+ track.current_time = current_time
+ track.video_duration = video_duration
+ track.last_access_time = timezone.now()
+ track.save()
+ success = True
+ else:
+ success = False
+ context = {"success": success}
+ return JsonResponse(context)
+
+
+@login_required
+@email_verified
+def view_lesson_watch_stats(request, course_id, lesson_id):
+ user = request.user
+ course = get_object_or_404(Course, pk=course_id)
+ if not course.is_creator(user) and not course.is_teacher(user):
+ raise Http404('This course does not belong to you')
+ trackings = TrackLesson.objects.get_queryset().filter(
+ course_id=course_id, lesson_id=lesson_id
+ ).order_by("id")
+ total = trackings.count()
+ paginator = Paginator(trackings, 30)
+ page = request.GET.get('page')
+ trackings = paginator.get_page(page)
+ context = {'objects': trackings, 'total': total, 'course_id': course_id,
+ 'lesson_id': lesson_id}
+ return render(request, 'view_lesson_tracking.html', context)
--
cgit
From 6b5b21fc26879c1724bf02952584196f6c302b91 Mon Sep 17 00:00:00 2001
From: adityacp
Date: Sat, 7 Nov 2020 11:58:22 +0530
Subject: Add tests for lesson tracking
---
stats/tests.py | 139 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
stats/views.py | 1 +
2 files changed, 138 insertions(+), 2 deletions(-)
(limited to 'stats')
diff --git a/stats/tests.py b/stats/tests.py
index 7ce503c..c256feb 100644
--- a/stats/tests.py
+++ b/stats/tests.py
@@ -1,3 +1,138 @@
-from django.test import TestCase
+# Python Imports
+import json
+
+# Django Imports
+from django.test import TestCase, Client
+from django.contrib.auth.models import User, Group
+from django.urls import reverse
+
+# Local Imports
+from stats.models import TrackLesson
+from yaksh.models import Course, Lesson, LearningUnit, LearningModule
+
+
+class TestTrackLesson(TestCase):
+ def setUp(self):
+ self.client = Client()
+ self.mod_group, created = Group.objects.get_or_create(name='moderator')
+ self.user_plaintext_pass = 'demo'
+ self.user = User.objects.create_user(
+ username='demo_user',
+ password=self.user_plaintext_pass,
+ first_name='first_name',
+ last_name='last_name',
+ email='demo@test.com',
+ )
+
+ # Create Student
+ self.student_plaintext_pass = 'demo_student'
+ self.student = User.objects.create_user(
+ username='demo_student',
+ password=self.student_plaintext_pass,
+ first_name='student_first_name',
+ last_name='student_last_name',
+ email='demo_student@test.com'
+ )
+
+ # Add to moderator group
+ self.mod_group.user_set.add(self.user)
+
+ self.course = Course.objects.create(
+ name="Test_course",
+ enrollment="Open Enrollment", creator=self.user
+ )
+ self.lesson = Lesson.objects.create(
+ name="Test_lesson", description="test description",
+ creator=self.user)
+ self.learning_unit = LearningUnit.objects.create(
+ order=0, type="lesson", lesson=self.lesson
+ )
+ self.learning_module = LearningModule.objects.create(
+ order=0, name="Test_module", description="Demo module",
+ check_prerequisite=False, creator=self.user
+ )
+ self.learning_module.learning_unit.add(self.learning_unit.id)
+ self.track = TrackLesson.objects.create(
+ user_id=self.student.id, course_id=self.course.id,
+ lesson_id=self.lesson.id
+ )
+
+ def tearDown(self):
+ self.client.logout()
+ self.mod_group.delete()
+ self.user.delete()
+ self.student.delete()
+ self.course.delete()
+ self.learning_unit.delete()
+ self.learning_module.delete()
+
+ def test_add_video_track(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+
+ # Student not enrolled in the course fails to add the tracking
+ response = self.client.post(
+ reverse('stats:add_tracker',
+ kwargs={"tracker_id": self.track.id}),
+ data={'video_duration': '00:05:00'}
+ )
+ self.assertEqual(response.status_code, 404)
+
+ self.course.students.add(self.student.id)
+ # No current time given in the post data
+ response = self.client.post(
+ reverse('stats:add_tracker',
+ kwargs={"tracker_id": self.track.id}),
+ data={'video_duration': '00:05:00'}
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertFalse(response.json().get('success'))
+
+ # Valid post data
+ response = self.client.post(
+ reverse('stats:add_tracker',
+ kwargs={"tracker_id": self.track.id}),
+ data={'video_duration': '00:05:00',
+ 'current_video_time': '00:01:00'}
+ )
+
+ self.assertEqual(response.status_code, 200)
+ self.assertTrue(response.json().get('success'))
+
+ def test_disallow_student_view_tracking(self):
+ self.client.login(
+ username=self.student.username,
+ password=self.student_plaintext_pass
+ )
+
+ # Fails to view the lesson data for student
+ response = self.client.get(
+ reverse('stats:view_lesson_watch_stats',
+ kwargs={"course_id": self.course.id,
+ "lesson_id": self.lesson.id})
+ )
+ self.assertEqual(response.status_code, 404)
+
+ def test_allow_moderator_view_tracking(self):
+ self.client.login(
+ username=self.user.username,
+ password=self.user_plaintext_pass
+ )
+ # Course creator can view the lesson data
+ response = self.client.get(
+ reverse('stats:view_lesson_watch_stats',
+ kwargs={"course_id": self.course.id,
+ "lesson_id": self.lesson.id})
+ )
+ response_data = response.context
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(response_data.get('total'), 1)
+ expected_tracker = list(TrackLesson.objects.filter(
+ user_id=self.student.id, course_id=self.course.id,
+ lesson_id=self.lesson.id).values_list("id", flat=True))
+ obtained_tracker = list(response_data.get(
+ 'objects').object_list.values_list("id", flat=True))
+ self.assertEqual(obtained_tracker, expected_tracker)
-# Create your tests here.
diff --git a/stats/views.py b/stats/views.py
index ddbc1b3..3bfe9c3 100644
--- a/stats/views.py
+++ b/stats/views.py
@@ -4,6 +4,7 @@ from django.http import JsonResponse
from django.utils import timezone
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator
+from django.http import Http404
# Local Imports
from stats.models import TrackLesson
--
cgit
From 7419b67b1928f30824a332d36afcdfddebaf2479 Mon Sep 17 00:00:00 2001
From: adityacp
Date: Sat, 7 Nov 2020 12:09:22 +0530
Subject: Add table sorter
---
stats/templates/view_lesson_tracking.html | 34 ++++++++++++++++---------------
1 file changed, 18 insertions(+), 16 deletions(-)
(limited to 'stats')
diff --git a/stats/templates/view_lesson_tracking.html b/stats/templates/view_lesson_tracking.html
index fd87d70..fa891e3 100644
--- a/stats/templates/view_lesson_tracking.html
+++ b/stats/templates/view_lesson_tracking.html
@@ -1,7 +1,9 @@
{% extends "manage.html" %}
-
+{% load static %}
{% block title %} Lesson Views {% endblock %}
{% block script %}
+
{% endblock %}
@@ -49,22 +40,22 @@
Started on |
Current Duration |
Video Duration |
- Percentage watched |
- Total visits |
+ Percentage Watched |
+ Watched Once Completely |
+ Total Time Spent |
{% for track in trackings %}
{{ forloop.counter0|add:objects.start_index }} |
{{track.user.get_full_name}} |
- {% with track.get_last_access_time_and_vists as time_and_visits %}
- {{time_and_visits.0}} |
+ {{track.get_last_access_time}} |
{{track.creation_time}} |
- {{track.current_time}} |
- {{track.video_duration}} |
- |
- {{time_and_visits.1}} |
- {% endwith %}
+ {{track.get_current_time}} |
+ {{track.get_video_duration}} |
+ {{track.get_percentage_complete}} |
+ {{track.get_watched}} |
+ {{track.time_spent}} |
{% endfor %}
@@ -72,4 +63,4 @@
{% include "yaksh/paginator.html" %}
-{% endblock %}
\ No newline at end of file
+{% endblock %}
diff --git a/stats/views.py b/stats/views.py
index f7c028f..53b7cf7 100644
--- a/stats/views.py
+++ b/stats/views.py
@@ -7,7 +7,7 @@ from django.core.paginator import Paginator
from django.http import Http404
# Local Imports
-from stats.models import TrackLesson, LessonLog
+from stats.models import TrackLesson, LessonLog, str_to_time, time_to_seconds
from yaksh.models import Course
from yaksh.decorators import email_verified
@@ -25,12 +25,16 @@ def add_tracker(request, tracker_id):
video_duration = request.POST.get('video_duration')
current_time = request.POST.get('current_video_time')
if current_time:
- track.current_time = current_time
+ track.set_current_time(current_time)
track.video_duration = video_duration
LessonLog.objects.create(
- track_id=track.id, last_access_time=timezone.now()
+ track_id=track.id, current_time=current_time,
+ last_access_time=timezone.now()
)
track.save()
+ if not track.watched:
+ track.set_watched()
+ track.save()
success = True
else:
success = False
--
cgit
From f29c1b21857b25db96f775d0e5df02b4a26eefd9 Mon Sep 17 00:00:00 2001
From: adityacp
Date: Thu, 12 Nov 2020 17:42:00 +0530
Subject: Add migrations in stats app
---
stats/migrations/0002_release_0_29_1.py | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
create mode 100644 stats/migrations/0002_release_0_29_1.py
(limited to 'stats')
diff --git a/stats/migrations/0002_release_0_29_1.py b/stats/migrations/0002_release_0_29_1.py
new file mode 100644
index 0000000..44f1a54
--- /dev/null
+++ b/stats/migrations/0002_release_0_29_1.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.0.7 on 2020-11-12 12:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('stats', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='lessonlog',
+ name='current_time',
+ field=models.CharField(default='00:00:00', max_length=20),
+ ),
+ migrations.AddField(
+ model_name='tracklesson',
+ name='watched',
+ field=models.BooleanField(default=False),
+ ),
+ ]
--
cgit