summaryrefslogtreecommitdiff
path: root/stats
diff options
context:
space:
mode:
Diffstat (limited to 'stats')
-rw-r--r--stats/__init__.py0
-rw-r--r--stats/admin.py14
-rw-r--r--stats/apps.py5
-rw-r--r--stats/migrations/0001_initial.py42
-rw-r--r--stats/migrations/0002_release_0_29_1.py23
-rw-r--r--stats/migrations/__init__.py0
-rw-r--r--stats/models.py140
-rw-r--r--stats/templates/view_lesson_tracking.html140
-rw-r--r--stats/test_models.py184
-rw-r--r--stats/tests.py138
-rw-r--r--stats/urls.py12
-rw-r--r--stats/views.py70
12 files changed, 768 insertions, 0 deletions
diff --git a/stats/__init__.py b/stats/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/stats/__init__.py
diff --git a/stats/admin.py b/stats/admin.py
new file mode 100644
index 0000000..16ca528
--- /dev/null
+++ b/stats/admin.py
@@ -0,0 +1,14 @@
+# Django Imports
+from django.contrib import admin
+
+# Local Imports
+from stats.models import TrackLesson
+
+
+class TrackLessonAdmin(admin.ModelAdmin):
+ search_fields = ['user__first_name', 'user__last_name', 'user__username',
+ 'course__name', 'lesson__name']
+ readonly_fields = ["course", "user", "lesson"]
+
+
+admin.site.register(TrackLesson, TrackLessonAdmin)
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/migrations/0001_initial.py b/stats/migrations/0001_initial.py
new file mode 100644
index 0000000..4bbae49
--- /dev/null
+++ b/stats/migrations/0001_initial.py
@@ -0,0 +1,42 @@
+# Generated by Django 3.0.7 on 2020-11-07 13:46
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ('yaksh', '0027_release_0_28_0'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='TrackLesson',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('current_time', models.CharField(default='00:00:00', max_length=100)),
+ ('video_duration', models.CharField(default='00:00:00', max_length=100)),
+ ('creation_time', models.DateTimeField(auto_now_add=True)),
+ ('course', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yaksh.Course')),
+ ('lesson', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='yaksh.Lesson')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('user', 'course', 'lesson')},
+ },
+ ),
+ migrations.CreateModel(
+ name='LessonLog',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('last_access_time', models.DateTimeField(default=django.utils.timezone.now)),
+ ('track', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='stats.TrackLesson')),
+ ],
+ ),
+ ]
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),
+ ),
+ ]
diff --git a/stats/migrations/__init__.py b/stats/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/stats/migrations/__init__.py
diff --git a/stats/models.py b/stats/models.py
new file mode 100644
index 0000000..84ac7ae
--- /dev/null
+++ b/stats/models.py
@@ -0,0 +1,140 @@
+# Python Imports
+import pandas as pd
+
+# Django Imports
+from django.db import models
+from django.utils import timezone
+from django.contrib.auth.models import User
+from django.db.models import F
+
+# Local Imports
+from yaksh.models import Course, Lesson
+
+
+def str_to_datetime(s):
+ return timezone.datetime.strptime(s, '%H:%M:%S')
+
+
+def str_to_time(s):
+ return timezone.datetime.strptime(s, '%H:%M:%S').time()
+
+
+def time_to_seconds(time):
+ return timezone.timedelta(hours=time.hour, minutes=time.minute,
+ seconds=time.second).total_seconds()
+
+
+class TrackLessonManager(models.Manager):
+ def get_percentage_data(self, tracked_lessons):
+ percentage_data = {"1": 0, "2": 0, "3": 0, "4": 0}
+ for tracked in tracked_lessons:
+ percent = tracked.get_percentage_complete()
+ if percent < 25:
+ percentage_data["1"] = percentage_data["1"] + 1
+ elif percent >= 25 and percent < 50:
+ percentage_data["2"] = percentage_data["2"] + 1
+ elif percent >= 50 and percent < 75:
+ percentage_data["3"] = percentage_data["3"] + 1
+ elif percent >= 75:
+ percentage_data["4"] = percentage_data["4"] + 1
+ return percentage_data
+
+
+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")
+ creation_time = models.DateTimeField(auto_now_add=True)
+ watched = models.BooleanField(default=False)
+
+ objects = TrackLessonManager()
+
+ class Meta:
+ unique_together = ('user', 'course', 'lesson')
+
+ def get_log_counter(self):
+ return self.lessonlog_set.count()
+
+ def get_current_time(self):
+ if self.current_time == '00:00:00':
+ return '00:00:00'
+ return self.current_time
+
+ def get_video_duration(self):
+ if self.video_duration == '00:00:00':
+ return '00:00:00'
+ return self.video_duration
+
+ def set_current_time(self, ct):
+ t = timezone.datetime.strptime(ct, '%H:%M:%S').time()
+ current = timezone.datetime.strptime(self.current_time,
+ '%H:%M:%S').time()
+ if t > current:
+ self.current_time = ct
+
+ def get_percentage_complete(self):
+ ctime = self.current_time
+ vduration = self.video_duration
+ if ctime == '00:00:00' and vduration == '00:00:00':
+ return 0
+ duration = str_to_time(vduration)
+ watch_time = str_to_time(ctime)
+ duration_seconds = time_to_seconds(duration)
+ watched_seconds = time_to_seconds(watch_time)
+ percentage = round((watched_seconds / duration_seconds) * 100)
+ return percentage
+
+ def get_last_access_time(self):
+ lesson_logs = self.lessonlog_set
+ last_access_time = self.creation_time
+ if lesson_logs.exists():
+ last_access_time = lesson_logs.last().last_access_time
+ return last_access_time
+
+ def set_watched(self):
+ ctime = self.current_time
+ vduration = self.video_duration
+ if ctime != '00:00:00' and vduration != '00:00:00':
+ duration = str_to_time(vduration)
+ watch_time = (str_to_datetime(ctime) + timezone.timedelta(
+ seconds=120)).time()
+ self.watched = watch_time >= duration
+
+ def get_watched(self):
+ if not self.watched:
+ self.set_watched()
+ self.save()
+ return self.watched
+
+ def time_spent(self):
+ if self.video_duration != '00:00:00':
+ hits = self.get_log_counter()
+ duration = str_to_time(self.video_duration)
+ hit_duration = int((time_to_seconds(duration))/4)
+ total_duration = hits * hit_duration
+ return str(timezone.timedelta(seconds=total_duration))
+ return self.get_current_time()
+
+ def get_no_of_vists(self):
+ lesson_logs = self.lessonlog_set.values("last_access_time").annotate(
+ visits=F('last_access_time')
+ )
+ df = pd.DataFrame(lesson_logs)
+ visits = 1
+ if not df.empty:
+ visits = df.groupby(
+ [df['visits'].dt.date]
+ ).first().count()['visits']
+ return visits
+
+ def __str__(self):
+ return (f"Track {self.lesson} in {self.course} "
+ f"for {self.user.get_full_name()}")
+
+
+class LessonLog(models.Model):
+ track = models.ForeignKey(TrackLesson, on_delete=models.CASCADE)
+ current_time = models.CharField(max_length=20, default="00:00:00")
+ last_access_time = models.DateTimeField(default=timezone.now)
diff --git a/stats/templates/view_lesson_tracking.html b/stats/templates/view_lesson_tracking.html
new file mode 100644
index 0000000..b59fa7a
--- /dev/null
+++ b/stats/templates/view_lesson_tracking.html
@@ -0,0 +1,140 @@
+{% extends "manage.html" %}
+{% load static %}
+{% block title %} Lesson Video Stats {% endblock %}
+{% block script %}
+<script type="text/javascript" src="{% static 'yaksh/js/jquery.tablesorter.min.js' %}">
+</script>
+<script type="text/javascript">
+ $(document).ready(function() {
+ $.tablesorter.addWidget({
+ id: "numbering",
+ format: function(table) {
+ var c = table.config;
+ $("tr:visible", table.tBodies[0]).each(function(i) {
+ $(this).find('td').eq(0).text(i + 1);
+ });
+ }
+ });
+ $("#stats-table").tablesorter({
+ headers: {0: { sorter: false }}, widgets: ['numbering']
+ });
+ });
+</script>
+{% endblock %}
+{% block content %}
+<div class="container-fluid">
+ {% with objects.object_list as trackings %}
+ <center>
+ <h3>Statistics for {% with trackings|first as entry %} {{entry.lesson}} {% endwith %}</h3>
+ </center>
+ <a class="btn btn-primary" href="{% url 'yaksh:get_course_modules' course_id %}">
+ <i class="fa fa-arrow-left"></i>&nbsp;Back
+ </a>
+ <br><br>
+ <div class="row">
+ <div class="card" style="margin: 1%">
+ <div class="col" id='barDiv1'></div>
+ </div>
+ <div class="card" style="margin: 1%">
+ <div class="col" id="barDiv2"></div>
+ </div>
+ <div class="card" style="margin: 1%">
+ <div class="col" id="barDiv3"></div>
+ </div>
+ </div>
+ <script type="text/javascript">
+ var config = {responsive: true, displayModeBar: false}
+ var data = [
+ {
+ x: ["Completed", "Not Completed"],
+ y: ["{{completion.0}}", "{{completion.1}}"],
+ type: 'bar'
+ }
+ ];
+ var layout = {
+ title: "Number of completions (Out of {{visits.2}})",
+ xaxis: {title: 'Completion status'},
+ yaxis: {title: 'Count'},
+ width: 400,
+ height: 400,
+ };
+ Plotly.newPlot('barDiv1', data, layout, config);
+ var data = [
+ {
+ x: ["Visited", "Not Visited"],
+ y: ["{{visits.0}}", "{{visits.1}}"],
+ type: 'bar'
+ }
+ ];
+ var layout = {
+ title: "Number of visits (Out of {{visits.2}})",
+ xaxis: {title: 'Visit status'},
+ yaxis: {title: 'Count'},
+ width: 400,
+ height: 400,
+ };
+ Plotly.newPlot('barDiv2', data, layout, config);
+ var x_data = ["0-25", "25-50", "50-75", "75-100"], y_data = [];
+ {% for i, j in percentage_data.items %}
+ y_data.push("{{j}}")
+ {% endfor %}
+ var data = [{x: x_data, y: y_data, type: 'bar'}];
+ var layout = {
+ title: "Range wise completion (Out of {{total}})",
+ xaxis: {title: 'Percentage Range'},
+ yaxis: {title: 'Count'},
+ width: 400,
+ height: 400,
+ };
+ Plotly.newPlot('barDiv3', data, layout, config);
+ </script>
+ <br>
+ {% include "yaksh/paginator.html" %}
+ <br>
+ <div class="table-responsive">
+ <table class="table table-dark table-responsive-sm" id="stats-table">
+ <thead class="thead-dark">
+ <tr>
+ <th>Sr No.</th>
+ <th>Student Name&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Last access on&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Started on&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Current Duration&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Video Duration&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Percentage&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Watched&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Total Time Spent&nbsp;<i class="fa fa-sort"></i></th>
+ <th>Total Visits&nbsp;<i class="fa fa-sort"></i></th>
+ </tr>
+ </thead>
+ <tbody class="list">
+ {% for track in trackings %}
+ <tr>
+ <td>{{ forloop.counter0 }}</td>
+ <td>{{track.user.get_full_name}}</td>
+ <td>{{track.get_last_access_time}}</td>
+ <td>{{track.creation_time}}</td>
+ <td>{{track.get_current_time}}</td>
+ <td>{{track.get_video_duration}}</td>
+ <td>{{track.get_percentage_complete}} %</td>
+ <td>
+ {% with track.get_watched as watched %}
+ {% if watched %}
+ <span class="badge-pill badge-success">{{watched}}</span>
+ {% else %}
+ <span class="badge-pill badge-warning">{{watched}}</span>
+ {% endif %}
+ {% endwith %}
+ </td>
+ <td>{{track.time_spent}}</td>
+ <td>{{track.get_no_of_vists}}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ {% endwith %}
+ <br>
+ {% include "yaksh/paginator.html" %}
+</div>
+{% endblock %}
diff --git a/stats/test_models.py b/stats/test_models.py
new file mode 100644
index 0000000..7f84330
--- /dev/null
+++ b/stats/test_models.py
@@ -0,0 +1,184 @@
+from django.test import TestCase
+from django.contrib.auth.models import User
+from django.utils import timezone
+from stats.models import TrackLesson, LessonLog
+from yaksh.models import Course, Lesson, LearningModule, LearningUnit
+
+
+class TrackLessonTestCase(TestCase):
+ def setUp(self):
+ creator = User.objects.create(username='creator', password='test',
+ email='test1@test.com')
+ self.student = User.objects.create(username='student', password='test',
+ email='test2@test.com')
+ self.course = Course.objects.create(
+ name="Test Course", enrollment="Enroll Request", creator=creator
+ )
+ learning_module = LearningModule.objects.create(
+ name='LM', description='module', creator=creator
+ )
+ self.lesson = Lesson.objects.create(
+ name='Lesson', description='Video Lesson', creator=creator
+ )
+ learning_unit = LearningUnit.objects.create(order=1, type='lesson',
+ lesson=self.lesson)
+ learning_module.learning_unit.add(learning_unit)
+ learning_module.save()
+ self.course.learning_module.add(learning_module)
+ self.course.students.add(self.student)
+ self.course.save()
+ self.tracker = TrackLesson.objects.create(user=self.student,
+ course=self.course,
+ lesson=self.lesson)
+ LessonLog.objects.create(track=self.tracker)
+ self.last_access_time = timezone.now()
+ LessonLog.objects.create(track=self.tracker,
+ last_access_time=self.last_access_time)
+
+ def tearDown(self):
+ User.objects.all().delete()
+ Course.objects.all().delete()
+ Lesson.objects.all().delete()
+ LearningUnit.objects.all().delete()
+ LearningModule.objects.all().delete()
+ LessonLog.objects.all().delete()
+ TrackLesson.objects.all().delete()
+
+ def test_track_lesson(self):
+ # Given
+ tracker = self.tracker
+
+ # Then
+ self.assertEqual(tracker.user, self.student)
+ self.assertEqual(tracker.course, self.course)
+ self.assertEqual(tracker.lesson, self.lesson)
+ self.assertEqual(tracker.current_time, '00:00:00')
+ self.assertEqual(tracker.video_duration, '00:00:00')
+ self.assertFalse(tracker.watched)
+
+ def test_log_counter(self):
+ # Given
+ tracker = self.tracker
+ expected_count = 2
+
+ # When
+ counts = tracker.get_log_counter()
+
+ # Then
+ self.assertEqual(counts, expected_count)
+
+ def test_get_current_time(self):
+ # Given
+ tracker = self.tracker
+ expected_time = '00:00:00'
+
+ # When
+ current_time = tracker.get_current_time()
+
+ # Then
+ self.assertEqual(current_time, expected_time)
+
+ def test_get_video_duration(self):
+ # Given
+ tracker = self.tracker
+ expected_duration = '00:00:00'
+
+ # When
+ duration = tracker.get_video_duration()
+
+ # Then
+ self.assertEqual(duration, expected_duration)
+
+ def test_set_current_time(self):
+ # Given
+ tracker = self.tracker
+ ctime = timezone.now()
+
+ # When
+ tracker.set_current_time(ctime.strftime('%H:%M:%S'))
+ tracker.save()
+ updated_time = tracker.get_current_time()
+
+ # Then
+ self.assertEqual(updated_time, ctime.strftime('%H:%M:%S'))
+
+ # Given
+ time_now = timezone.now()
+ invalid_ctime = ctime - timezone.timedelta(seconds=100)
+
+ # When
+ tracker.set_current_time(invalid_ctime.strftime('%H:%M:%S'))
+ tracker.save()
+ old_time = tracker.get_current_time()
+
+ # Then
+ self.assertEqual(old_time, ctime.strftime('%H:%M:%S'))
+
+ def test_get_percentage_complete(self):
+ # Given
+ tracker = self.tracker
+ expected_percentage = 0
+
+ # When
+ percentage = tracker.get_percentage_complete()
+
+ # Then
+ self.assertEqual(percentage, expected_percentage)
+
+ # Given
+ expected_percentage = 75
+
+ # When
+ tracker.set_current_time('00:03:00')
+ tracker.video_duration = '00:04:00'
+ tracker.save()
+ percentage = tracker.get_percentage_complete()
+
+ # Then
+ self.assertEqual(percentage, expected_percentage)
+
+ def test_get_last_access_time(self):
+ # Given
+ tracker = self.tracker
+ expected_time = self.last_access_time
+
+ # When
+ time = tracker.get_last_access_time()
+
+ # Then
+ self.assertEqual(time, expected_time)
+
+ def test_set_get_watched(self):
+ # Given
+ tracker = self.tracker
+
+ # When
+ tracker.set_watched()
+
+ # Then
+ self.assertFalse(tracker.get_watched())
+
+ # Given
+ tracker = self.tracker
+
+ # When
+ tracker.set_current_time('00:03:55')
+ tracker.video_duration = '00:04:00'
+ tracker.save()
+ tracker.set_watched()
+
+ # Then
+ self.assertTrue(tracker.get_watched())
+
+ def test_time_spent(self):
+ # Given
+ tracker = self.tracker
+ expected_time = '00:02:00'
+
+ # When
+ tracker.video_duration = '00:04:00'
+ tracker.save()
+ time = tracker.time_spent()
+
+ # Then
+ self.assertTrue(expected_time, time)
diff --git a/stats/tests.py b/stats/tests.py
new file mode 100644
index 0000000..540ff4d
--- /dev/null
+++ b/stats/tests.py
@@ -0,0 +1,138 @@
+# 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))
+ obtained_tracker = list(response_data.get(
+ 'objects').object_list)
+ self.assertEqual(obtained_tracker, expected_tracker)
+
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/<int:tracker_id>',
+ views.add_tracker, name='add_tracker'),
+ path('view/watch/stats/<int:course_id>/<int:lesson_id>',
+ 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..a5cdeb7
--- /dev/null
+++ b/stats/views.py
@@ -0,0 +1,70 @@
+# 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
+from django.http import Http404
+
+# Local Imports
+from stats.models import TrackLesson, LessonLog, str_to_time, time_to_seconds
+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.set_current_time(current_time)
+ track.video_duration = video_duration
+ track.save()
+ if not track.watched:
+ track.set_watched()
+ track.save()
+ LessonLog.objects.create(
+ track_id=track.id, current_time=current_time,
+ last_access_time=timezone.now()
+ )
+ 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.objects.prefetch_related("students"), id=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")
+ percentage_data = TrackLesson.objects.get_percentage_data(trackings)
+ visited = trackings.count()
+ completed = trackings.filter(watched=True).count()
+ students_total = course.students.count()
+ paginator = Paginator(trackings, 30)
+ page = request.GET.get('page')
+ trackings = paginator.get_page(page)
+ context = {
+ 'objects': trackings, 'total': visited, 'course_id': course_id,
+ 'lesson_id': lesson_id, "percentage_data": percentage_data,
+ 'completion': [completed, students_total-completed, students_total],
+ 'visits': [visited, students_total-visited, students_total]
+ }
+ return render(request, 'view_lesson_tracking.html', context)