Skip to content

Commit

Permalink
user analytics backend
Browse files Browse the repository at this point in the history
  • Loading branch information
rachllee committed Mar 27, 2024
1 parent dc613a9 commit 3fc9869
Show file tree
Hide file tree
Showing 10 changed files with 1,701 additions and 1,127 deletions.
2 changes: 1 addition & 1 deletion backend/Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,4 @@ typing-extensions = "*"
drf-excel = "*"

[requires]
python_version = "3"
python_version = "3"
2,352 changes: 1,227 additions & 1,125 deletions backend/Pipfile.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions backend/ohq/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Question,
Queue,
QueueStatistic,
UserStatistic,
Semester,
Tag,
)
Expand All @@ -26,3 +27,4 @@
admin.site.register(QueueStatistic)
admin.site.register(Announcement)
admin.site.register(Tag)
admin.site.register(UserStatistic)
67 changes: 67 additions & 0 deletions backend/ohq/management/commands/user_stat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
import logging

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

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)


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

def calculate_statistics(self, profiles, courses, earliest_date):
yesterday = timezone.datetime.today().date() - timezone.timedelta(days=1)
active_users_today = set()

for course in courses:
logger.debug("course here is", course)
if earliest_date:
date = earliest_date
else:
course_questions = Question.objects.filter(queue__course=course)
date = (
timezone.template_localtime(
course_questions.earliest("time_asked").time_asked
).date()
if course_questions
else yesterday
)

course_questions = Question.objects.filter(queue__course=course, time_asked__gte=date)
for q in course_questions:
active_users_today.add(q.asked_by)
active_users_today.add(q.responded_to_by)

for profile in profiles:
if profile.user in active_users_today:
user_calculate_questions_asked(profile.user)
user_calculate_questions_answered(profile.user)
user_calculate_time_helped(profile.user)
user_calculate_time_helping(profile.user)
user_calculate_students_helped(profile.user)


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

self.calculate_statistics(profiles, courses, earliest_date)


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

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


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")
]



69 changes: 68 additions & 1 deletion backend/ohq/statistics.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
from django.db.models import Avg, Case, Count, F, Sum, When
from django.db.models.functions import TruncDate
from django.utils import timezone
from decimal import Decimal, ROUND_HALF_UP

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 +233,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 = Decimal(Question.objects.filter(asked_by=user).count())
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 = Decimal(Question.objects.filter(responded_to_by=user).count())
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)
.aggregate(time_helped=Sum(F("time_responded_to") - F("time_response_started")))
)
time = user_time_helped["time_helped"]

UserStatistic.objects.update_or_create(
user=user,
metric=UserStatistic.METRIC_TOTAL_TIME_BEING_HELPED,
defaults={"value": time.seconds if time else Decimal('0.00000000')},
)


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

UserStatistic.objects.update_or_create(
user=user,
metric=UserStatistic.METRIC_TOTAL_TIME_HELPING,
defaults={"value": time.seconds if time else Decimal('0.00000000')},
)


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



27 changes: 27 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"devDependencies": {
"@types/node": "^20.11.30"
}
}
Loading

0 comments on commit 3fc9869

Please sign in to comment.