Skip to content

Commit

Permalink
Merge pull request #309 from pennlabs/user-analytics
Browse files Browse the repository at this point in the history
Add User Lifetime Statistics
  • Loading branch information
rachllee authored Apr 14, 2024
2 parents dde9f16 + 247e26c commit b6b6d86
Show file tree
Hide file tree
Showing 8 changed files with 474 additions and 2 deletions.
2 changes: 2 additions & 0 deletions backend/ohq/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
QueueStatistic,
Semester,
Tag,
UserStatistic,
)


Expand All @@ -26,3 +27,4 @@
admin.site.register(QueueStatistic)
admin.site.register(Announcement)
admin.site.register(Tag)
admin.site.register(UserStatistic)
7 changes: 6 additions & 1 deletion backend/ohq/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ class QuestionSearchFilter(filters.FilterSet):

class Meta:
model = Question
fields = {"time_asked": ["gt", "lt"], "queue": ["exact"], "status": ["exact"], "time_responded_to": ["gt", "lt"]}
fields = {
"time_asked": ["gt", "lt"],
"queue": ["exact"],
"status": ["exact"],
"time_responded_to": ["gt", "lt"],
}

def search_filter(self, queryset, name, value):
return queryset.filter(
Expand Down
52 changes: 52 additions & 0 deletions backend/ohq/management/commands/user_stat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import datetime
from datetime import timedelta

from django.contrib.auth import get_user_model
from django.core.management.base import BaseCommand
from django.db.models import Q
from django.utils import timezone

from ohq.models import Course, Question
from ohq.statistics import (
user_calculate_questions_answered,
user_calculate_questions_asked,
user_calculate_students_helped,
user_calculate_time_helped,
user_calculate_time_helping,
)


User = get_user_model()


class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("--hist", action="store_true", help="Calculate all historic statistics")

def calculate_statistics(self, courses, earliest_date):
for course in courses:

questions_queryset = Question.objects.filter(
queue__course=course, time_asked__gte=earliest_date
)
users_union = User.objects.filter(
Q(id__in=questions_queryset.values_list("asked_by", flat=True))
| Q(id__in=questions_queryset.values_list("responded_to_by", flat=True))
)

for user in users_union:
user_calculate_questions_asked(user)
user_calculate_questions_answered(user)
user_calculate_time_helped(user)
user_calculate_time_helping(user)
user_calculate_students_helped(user)

def handle(self, *args, **kwargs):
if kwargs["hist"]:
courses = Course.objects.all()
earliest_date = timezone.make_aware(datetime.datetime.utcfromtimestamp(0))
else:
courses = Course.objects.filter(archived=False)
earliest_date = timezone.now().date() - timedelta(days=1)

self.calculate_statistics(courses, earliest_date)
53 changes: 53 additions & 0 deletions backend/ohq/migrations/0020_auto_20240326_0226.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 3.1.7 on 2024-03-26 02:26

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("ohq", "0019_auto_20211114_1800"),
]

operations = [
migrations.CreateModel(
name="UserStatistic",
fields=[
(
"id",
models.AutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"metric",
models.CharField(
choices=[
("TOTAL_QUESTIONS_ASKED", "Total questions asked"),
("TOTAL_QUESTIONS_ANSWERED", "Total questions answered"),
("TOTAL_TIME_BEING_HELPED", "Total time being helped"),
("TOTAL_TIME_HELPING", "Total time helping"),
("TOTAL_STUDENTS_HELPED", "Total students helped"),
],
max_length=256,
),
),
("value", models.DecimalField(decimal_places=8, max_digits=16)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
),
migrations.AddConstraint(
model_name="userstatistic",
constraint=models.UniqueConstraint(
fields=("user", "metric"), name="unique_user_statistic"
),
),
]
32 changes: 32 additions & 0 deletions backend/ohq/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,3 +411,35 @@ class Announcement(models.Model):
author = models.ForeignKey(User, related_name="announcements", on_delete=models.CASCADE)
time_updated = models.DateTimeField(auto_now=True)
course = models.ForeignKey(Course, related_name="announcements", on_delete=models.CASCADE)


class UserStatistic(models.Model):
"""
Statistics related to a user (student or TA) across many courses
"""

METRIC_TOTAL_QUESTIONS_ASKED = "TOTAL_QUESTIONS_ASKED"
METRIC_TOTAL_QUESTIONS_ANSWERED = "TOTAL_QUESTIONS_ANSWERED"
METRIC_TOTAL_TIME_BEING_HELPED = "TOTAL_TIME_BEING_HELPED"
METRIC_TOTAL_TIME_HELPING = "TOTAL_TIME_HELPING"
METRIC_TOTAL_STUDENTS_HELPED = "TOTAL_STUDENTS_HELPED"

METRIC_CHOICES = [
(METRIC_TOTAL_QUESTIONS_ASKED, "Total questions asked"),
(METRIC_TOTAL_QUESTIONS_ANSWERED, "Total questions answered"),
(METRIC_TOTAL_TIME_BEING_HELPED, "Total time being helped"),
(METRIC_TOTAL_TIME_HELPING, "Total time helping"),
(METRIC_TOTAL_STUDENTS_HELPED, "Total students helped"),
]

user = models.ForeignKey(User, on_delete=models.CASCADE)
metric = models.CharField(max_length=256, choices=METRIC_CHOICES)
value = models.DecimalField(max_digits=16, decimal_places=8)

class Meta:
constraints = [
models.UniqueConstraint(fields=["user", "metric"], name="unique_user_statistic")
]

def __str__(self):
return f"{self.user}: {self.metric}"
71 changes: 70 additions & 1 deletion backend/ohq/statistics.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import math
from decimal import Decimal

from django.contrib.auth import get_user_model
from django.db.models import Avg, Case, Count, F, Sum, When
from django.db.models.functions import TruncDate
from django.utils import timezone

from ohq.models import CourseStatistic, Question, QueueStatistic
from ohq.models import CourseStatistic, Question, QueueStatistic, UserStatistic


User = get_user_model()
Expand Down Expand Up @@ -232,3 +235,69 @@ def queue_calculate_questions_per_ta_heatmap(queue, weekday, hour):
hour=hour,
defaults={"value": statistic if statistic else 0},
)


def user_calculate_questions_asked(user):
num_questions = Question.objects.filter(asked_by=user).count()

if num_questions:
UserStatistic.objects.update_or_create(
user=user,
metric=UserStatistic.METRIC_TOTAL_QUESTIONS_ASKED,
defaults={"value": num_questions},
)


def user_calculate_questions_answered(user):
num_questions = Question.objects.filter(
responded_to_by=user, status=Question.STATUS_ANSWERED
).count()

if num_questions:
UserStatistic.objects.update_or_create(
user=user,
metric=UserStatistic.METRIC_TOTAL_QUESTIONS_ANSWERED,
defaults={"value": num_questions},
)


def user_calculate_time_helped(user):
user_time_helped = Question.objects.filter(
asked_by=user, status=Question.STATUS_ANSWERED
).aggregate(time_helped=Sum(F("time_responded_to") - F("time_response_started")))
time = user_time_helped["time_helped"]

if time and not math.isclose(time.total_seconds(), 0, abs_tol=0.001):
UserStatistic.objects.update_or_create(
user=user,
metric=UserStatistic.METRIC_TOTAL_TIME_BEING_HELPED,
defaults={"value": time.seconds},
)


def user_calculate_time_helping(user):
user_time_helping = Question.objects.filter(
responded_to_by=user, status=Question.STATUS_ANSWERED
).aggregate(time_answering=Sum(F("time_responded_to") - F("time_response_started")))
time = user_time_helping["time_answering"]

if time and not math.isclose(time.seconds, 0, abs_tol=0.001):
UserStatistic.objects.update_or_create(
user=user,
metric=UserStatistic.METRIC_TOTAL_TIME_HELPING,
defaults={"value": time.seconds},
)


def user_calculate_students_helped(user):
num_students = Decimal(
Question.objects.filter(status=Question.STATUS_ANSWERED, responded_to_by=user)
.distinct("asked_by")
.count()
)
if num_students:
UserStatistic.objects.update_or_create(
user=user,
metric=UserStatistic.METRIC_TOTAL_STUDENTS_HELPED,
defaults={"value": num_students},
)
Loading

0 comments on commit b6b6d86

Please sign in to comment.