Song reporting and user stats

This commit is contained in:
Olaf Slomp
2018-12-14 16:59:44 +01:00
committed by Daan Sprenkels
parent 0c1f9cb08d
commit f6fcc63450
17 changed files with 381 additions and 47 deletions

View File

@ -11,6 +11,7 @@ urlpatterns = [
url(r'^managesongs', views.managesongs), url(r'^managesongs', views.managesongs),
url(r'^queue', views.queue), url(r'^queue', views.queue),
url(r'^request', views.request), url(r'^request', views.request),
url(r'^report', views.report),
url(r'^skip', views.skip), url(r'^skip', views.skip),
url(r'^moveup', views.move_up), url(r'^moveup', views.move_up),
url(r'^movedown', views.move_down), url(r'^movedown', views.move_down),

View File

@ -232,6 +232,17 @@ def request(request):
return JsonResponse({ 'success': True }) return JsonResponse({ 'success': True })
@require_http_methods(["POST"])
@api_auth_required
def report(request):
queue = request.user.queue
song = get_object_or_404(Song, id=request.POST.get('id'), deleted=False)
msg = request.POST.get('msg')
err = song.report(request.user, msg)
return JsonResponse({ 'success': True })
@require_http_methods(["POST"]) @require_http_methods(["POST"])
@api_auth_required @api_auth_required
def upload(request): def upload(request):

View File

@ -114,6 +114,8 @@ USE_TZ = True
# zc files (CSS, JavaScript, Images) # zc files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/ # https://docs.djangoproject.com/en/1.10/howto/static-files/
BASE_URL = 'https://marietje-zuid.science.ru.nl'
STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATIC_URL = '/static/' STATIC_URL = '/static/'
LOGIN_URL = '/login/' LOGIN_URL = '/login/'

View File

@ -45,6 +45,22 @@ $(function () {
return false; return false;
}); });
$(document).on('click', '[data-report-song-id]', function () {
var songId = $(this).data('report-song-id');
var message = prompt("What is wrong with the song?");
if (message == "") {
alert("Please enter a message.");
return false
}
$.post('/api/report', {id: songId, msg: message, csrfmiddlewaretoken: csrf_token}, function (result) {
console.log(result);
if (result.success) {
alert("Thanks for the report!");
}
});
return false;
});
$('#cancel-request').click(function () { $('#cancel-request').click(function () {
hideRequestTable(); hideRequestTable();
}); });
@ -267,7 +283,11 @@ function getSongs()
$.each(songs, function (id, song) { $.each(songs, function (id, song) {
var artist = song.artist.trim() === '' ? '?' : song.artist; var artist = song.artist.trim() === '' ? '?' : song.artist;
var title = song.title.trim() === '' ? '?' : song.title; var title = song.title.trim() === '' ? '?' : song.title;
$('#request-table tbody:last-child').append('<tr><td>' + artist + '</td><td><a href="#" data-song-id="' + song.id + '">' + title + '</a></td><td>' + (song.uploader_name ? song.uploader_name : 'Marietje') + '</td><td style="text-align: right;">' + song.duration.secondsToMMSS() + '</td></tr>'); $('#request-table tbody:last-child').append('<tr><td>' + artist +
'</td><td><a href="#" data-song-id="' + song.id + '">' + title +
'</a></td><td>' + (song.uploader_name ? song.uploader_name : 'Marietje') +
'</td><td style="text-align: right;">' + song.duration.secondsToMMSS() +
'</td><td><a href="#" data-report-song-id="'+ song.id + '">⚑</a></td></tr>');
}); });
var pageNumSelect = $('.pagenum'); var pageNumSelect = $('.pagenum');
pageNumSelect.empty(); pageNumSelect.empty();
@ -290,6 +310,7 @@ function getSongs()
}); });
} }
function showRequestTable() function showRequestTable()
{ {
$('#request-button').text('Close'); $('#request-button').text('Close');

View File

@ -35,6 +35,8 @@
<li{% if request.path == url %} class="active"{% endif %}><a href="{{ url }}">Manage</a></li> <li{% if request.path == url %} class="active"{% endif %}><a href="{{ url }}">Manage</a></li>
{% url 'stats:stats' as url %} {% url 'stats:stats' as url %}
<li{% if request.path == url %} class="active"{% endif %}><a href="{{ url }}">Stats</a></li> <li{% if request.path == url %} class="active"{% endif %}><a href="{{ url }}">Stats</a></li>
{% url 'stats:user_stats' as url %}
<li{% if request.path == url %} class="active"{% endif %}><a href="{{ url }}">User Stats</a></li>
{% if user.is_staff %} {% if user.is_staff %}
{% url 'admin:index' as url %} {% url 'admin:index' as url %}
<li><a href="{{ url }}">Admin</a></li> <li><a href="{{ url }}">Admin</a></li>

View File

@ -1,9 +1,12 @@
import time
from django.db import models from django.db import models
from django.db.models import Q, Max from django.db.models import Q, Max
from django.conf import settings from django.conf import settings
from songs.models import Song
from django.utils import timezone from django.utils import timezone
from songs.models import Song
class Playlist(models.Model): class Playlist(models.Model):
def __str__(self): def __str__(self):

View File

@ -15,6 +15,7 @@
<th>Title</th> <th>Title</th>
<th>Uploader</th> <th>Uploader</th>
<th style="text-align: right;">Length</th> <th style="text-align: right;">Length</th>
<th>Report</th>
</tr> </tr>
<tr> <tr>
<th colspan="2"><input id="search-all" class="search-input" type="text"></th> <th colspan="2"><input id="search-all" class="search-input" type="text"></th>

View File

@ -1,20 +1,33 @@
from django.contrib import admin from django.contrib import admin
from .models import Song from .models import ReportNote, Song
class ReportNoteInline(admin.StackedInline):
model = ReportNote
extra = 0
@admin.register(Song) @admin.register(Song)
class SongAdmin(admin.ModelAdmin): class SongAdmin(admin.ModelAdmin):
list_display = ('artist', 'title', 'user_name') list_display = ('artist', 'title', 'user_name', 'reports')
search_fields = ('artist', 'title', 'user__name') search_fields = ('artist', 'title', 'user__name')
inlines = [ReportNoteInline]
def reports(self, song):
return ReportNote.objects.filter(song=song).count()
@staticmethod @staticmethod
def user_name(obj): def user_name(song):
try: try:
return obj.user.name return song.user.name
except AttributeError: except AttributeError:
return '<unknown>' return '<unknown>'
@staticmethod @staticmethod
def get_readonly_fields(request, obj=None): def get_readonly_fields(request, obj=None):
return [] if request.user.is_superuser else ['hash'] return [] if request.user.is_superuser else ['hash']
@admin.register(ReportNote)
class ReportNoteAdmin(admin.ModelAdmin):
list_display = ('song', 'note', 'user')
search_fields = ('song__artist', 'song__title', 'user__name')

View File

@ -0,0 +1,14 @@
from django.conf import settings
from django.core.management.base import BaseCommand
from songs.models import ReportNote
class Command(BaseCommand):
help = 'Gather all song reports'
def handle(self, *args, **options):
reports = ReportNote.objects.all()
for report in reports:
song = report.song
url = '<{base_url}/admin/songs/song/{r.song.id}/change/>'.format(base_url=settings.BASE_URL, r=report)
print('Song: {r.song.artist} - {r.song.title}\nMessage: {r.note}\nLink: {url}'.format(url=url, r=report))
print('-' * 72)

View File

@ -0,0 +1,25 @@
# Generated by Django 2.1.2 on 2018-12-10 14:23
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),
('songs', '0003_search_index'),
]
operations = [
migrations.CreateModel(
name='ReportNote',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('note', models.TextField(blank=True, help_text='reason for edit request', verbose_name='reason')),
('song', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='songs.Song')),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -35,5 +35,27 @@ class Song(models.Model):
db_index=True, db_index=True,
help_text='hide this song from the search listings') help_text='hide this song from the search listings')
def report(self, user, note):
report_note = ReportNote(song=self, user=user, note=note)
report_note.save()
def __str__(self): def __str__(self):
return self.artist + ' - ' + self.title return self.artist + ' - ' + self.title
class ReportNote(models.Model):
song = models.ForeignKey(Song,
on_delete=models.CASCADE,
blank=False,
null=False,
db_index=True)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
blank=True,
null=True,
db_index=True)
note = models.TextField(verbose_name='reason', blank=True,
help_text='reason for edit request')
def __str__(self):
return "{song.artist} - {song.title}: '{note}'".format(song=self.song, note=self.note)

View File

@ -0,0 +1,9 @@
from django.core.management.base import BaseCommand
from stats.utils import recache_user_stats
class Command(BaseCommand):
help = 'Update the statistics cache'
def handle(self, *args, **options):
recache_user_stats()

View File

@ -26,7 +26,7 @@
<tr> <tr>
<th>#</th> <th>#</th>
<th>User</th> <th>User</th>
<th>#&nbsp;Songs</th> <th># Songs</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -51,7 +51,7 @@
<tr> <tr>
<th>#</th> <th>#</th>
<th>User</th> <th>User</th>
<th>#&nbsp;Requests</th> <th># Requests</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -67,7 +67,7 @@
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Time Requested</h2> <h2>Time requested</h2>
<h4>Total: {{stats.total_time_requested}}</h4> <h4>Total: {{stats.total_time_requested}}</h4>
<h4>Top {{ stats.stats_top_count }}:</h4> <h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive"> <div class="table-responsive">
@ -93,7 +93,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Unique requests</h2> <h2>Unique requests</h2>
<h4>Total: {{stats.total_unique_requests}}</h4> <h4>Total: {{stats.total_unique_requests.total}}</h4>
<h4>Top {{ stats.stats_top_count }}:</h4> <h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-striped"> <table class="table table-striped">
@ -101,7 +101,7 @@
<tr> <tr>
<th>#</th> <th>#</th>
<th>User</th> <th>User</th>
<th>#&nbsp;Requests</th> <th># Unique</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -109,7 +109,7 @@
<tr> <tr>
<th>{{ forloop.counter }}</th> <th>{{ forloop.counter }}</th>
<td>{{ stat.user__name }}</td> <td>{{ stat.user__name }}</td>
<td>{{ stat.total }} ({{ stat.ratio }}%)</td> <td>{{ stat.unique_requests }} ({% widthratio stat.unique_requests stat.total_requests 100 %}%)</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -126,7 +126,7 @@
<th>#</th> <th>#</th>
<th>Artist</th> <th>Artist</th>
<th>Title</th> <th>Title</th>
<th>#&nbsp;Requests</th> <th># Requests</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -141,6 +141,30 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<div class="col-md-6">
<h2>Most played uploaders</h2>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>User</th>
<th># Songs</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_requested_uploaders %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__user__name }}</td>
<td>{{ stat.total }} ({% widthratio stat.total stats.total_requests 100 %}%)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>Most played songs last 14 days</h2> <h2>Most played songs last 14 days</h2>
@ -152,7 +176,7 @@
<th>#</th> <th>#</th>
<th>Artist</th> <th>Artist</th>
<th>Title</th> <th>Title</th>
<th>#&nbsp;Requests</th> <th># Requests</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View File

@ -0,0 +1,99 @@
{% extends 'base.html' %}
{% load static %}
{% load tz %}
{% block title %}User Stats{% endblock %}
{% block content %}
<h1>User Statistics</h1>
<div class="row">
{% if current_age_text %}
<div class="alert alert-info">
<strong>{{ current_age_text }}</strong>
{% endif %}
</div>
<h4>Total uploads: {{ stats.total_uploads }}</h4>
<h4>Total requests: {{ stats.total_requests }}</h4>
<h4>Unique requests: {{ stats.unique_requests }} ({% widthratio stats.unique_requests stats.total_requests 100 %}%)</h4>
<h4>Total requested uploads: {{stats.total_played_uploads}}</h4>
<div class="col-md-6">
<h2>Most played songs</h2>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_songs %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td>{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h2>Most played uploads</h2>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploads %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__artist }}</td>
<td>{{ stat.song__title }}</td>
<td>{{ stat.total }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Most played uploaders</h2>
<h4>Top {{ stats.stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Uploader</th>
<th># Requests</th>
</tr>
</thead>
<tbody>
{% for stat in stats.most_played_uploaders %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.song__user__name }}</td>
<td>{{ stat.total }} ({% widthratio stat.total total_requests 100 %}%)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -6,4 +6,5 @@ app_name = 'stats'
urlpatterns = [ urlpatterns = [
url(r'^$', views.stats, name='stats'), url(r'^$', views.stats, name='stats'),
url(r'^user$', views.user_stats, name='user_stats'),
] ]

View File

@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, time
from django.core.cache import caches from django.core.cache import caches
from django.conf import settings from django.conf import settings
@ -8,19 +8,29 @@ from django.utils import timezone
from queues.models import PlaylistSong from queues.models import PlaylistSong
from songs.models import Song from songs.models import Song
from marietje.models import User
def recache_stats(): def recache_stats():
new_stats = compute_stats() new_stats = compute_stats()
caches['default'].delete('stats') caches['default'].delete('stats')
caches['default'].set('stats', new_stats, 7200) caches['default'].set('stats', new_stats, 2 * 3600)
return new_stats
def recache_user_stats():
users = User.objects.exclude(Q(id=None)
| Q(id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values('id')
for user in users:
new_stats = user_stats(user['id'])
cacheloc = 'userstats_{}'.format(user['id'])
caches['default'].delete(cacheloc)
caches['default'].set(cacheloc, new_stats, 48 * 3600)
return new_stats return new_stats
def to_days(time): def to_days(time):
for tr in time: for tr in time:
tr['duration'] = str(round(tr['total']/86400, 2)) + ' days' tr['duration'] = str(round(tr['total'] / 86400, 2)) + ' days'
return time return time
def compute_stats(): def compute_stats():
# We want to grab the time now, because otherwise we would be reporting a minute too late # We want to grab the time now, because otherwise we would be reporting a minute too late
@ -48,13 +58,14 @@ def compute_stats():
Q(user_id=None) Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values( | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'user__id', 'user__name').annotate( 'user__id', 'user__name').annotate(
total=Count('song_id', distinct=True), total_requests=Count('id', distinct=True),
ratio=Count('song_id', distinct=True) / Count('id') * unique_requests=Count('song__id', distinct=True)).order_by(
100).order_by('-total')[:settings.STATS_TOP_COUNT] '-total_requests')[:settings.STATS_TOP_COUNT]
total_unique_requests = PlaylistSong.objects.filter(state=2).exclude( total_unique_requests = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None) Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).distinct().count() | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).aggregate(
total=Count('song__id', distinct=True))
most_played_songs = PlaylistSong.objects.filter(state=2).exclude( most_played_songs = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None) Q(user_id=None)
@ -69,18 +80,23 @@ def compute_stats():
'song__artist', 'song__artist',
'song__title').annotate(total=Count('id')).order_by( 'song__title').annotate(total=Count('id')).order_by(
'-total', 'song__artist')[:settings.STATS_TOP_COUNT] '-total', 'song__artist')[:settings.STATS_TOP_COUNT]
time_requested = PlaylistSong.objects.filter(state=2).exclude( time_requested = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None) Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values( | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'user__id', 'user__name').annotate(total=Sum('song__duration')).order_by( 'user__id', 'user__name').annotate(total=Sum('song__duration')).order_by(
'-total')[:settings.STATS_TOP_COUNT] '-total')[:settings.STATS_TOP_COUNT]
total_time_requested = PlaylistSong.objects.all().filter(state=2).exclude( total_time_requested = PlaylistSong.objects.all().filter(state=2).exclude(
Q(user_id=None) Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).aggregate( | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).aggregate(
total=Sum('song__duration')) total=Sum('song__duration'))
most_requested_uploaders = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'song__user__id', 'song__user__name').annotate(total=Count(
'song__user__id')).order_by('-total')[:settings.STATS_TOP_COUNT]
return { return {
'last_updated': last_updated, 'last_updated': last_updated,
@ -93,7 +109,59 @@ def compute_stats():
'most_played_songs': list(most_played_songs), 'most_played_songs': list(most_played_songs),
'most_played_songs_14_days': list(most_played_songs_14_days), 'most_played_songs_14_days': list(most_played_songs_14_days),
'time_requested': to_days(list(time_requested)), 'time_requested': to_days(list(time_requested)),
'total_time_requested': str(round(float(total_time_requested['total'])/86400, 2)) + ' days', 'total_time_requested': str(round(float(total_time_requested['total']) / 86400, 2)) + ' days',
'stats_top_count': settings.STATS_TOP_COUNT, 'stats_top_count': settings.STATS_TOP_COUNT,
'most_requested_uploaders': list(most_requested_uploaders),
} }
def user_stats(request):
last_updated = datetime.now()
total_uploads = Song.objects.filter(
user__id=request, deleted=False).count()
total_requests = PlaylistSong.objects.filter(
user__id=request, state=2).count()
unique_requests = PlaylistSong.objects.filter(
user__id=request, state=2,
song_id__isnull=False).values('song_id').distinct().count()
most_played_songs = PlaylistSong.objects.filter(
user__id=request, state=2).exclude(
Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'song__artist',
'song__title').annotate(total=Count('id')).order_by(
'-total', 'song__artist', 'song__title')
most_played_uploaders = PlaylistSong.objects.filter(
user__id=request, state=2).exclude(
Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'song__user__id', 'song__user__name').annotate(
total=Count('song__user__id')).order_by('-total')
most_played_uploads = PlaylistSong.objects.filter(
state=2, song_id__in=Song.objects.filter(user__id=request)).exclude(
Q(user_id=None)
| Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'pk').values(
'song__artist',
'song__title').annotate(total=Count('id')).order_by(
'-total', 'song__artist', 'song__title')
total_played_uploads = most_played_uploads.aggregate(newtotal=Sum('total'))
return {
'last_updated': last_updated,
'total_uploads': total_uploads,
'total_requests': total_requests,
'unique_requests': unique_requests,
'most_played_songs': list(most_played_songs),
'most_played_uploaders': list(most_played_uploaders),
'most_played_uploads': list(most_played_uploads),
'stats_top_count': settings.STATS_TOP_COUNT,
'total_played_uploads': total_played_uploads['newtotal'],
}

View File

@ -2,6 +2,8 @@ from datetime import datetime, timedelta
from django.core.cache import caches from django.core.cache import caches
from django.shortcuts import render from django.shortcuts import render
from stats.utils import user_stats
from django.http import JsonResponse, HttpResponseForbidden
def stats(request): def stats(request):
@ -12,17 +14,33 @@ def stats(request):
if stats: if stats:
status = 200 status = 200
if 'last_updated' in stats: current_age_text = age_text(stats['last_updated'])
current_age = datetime.now() - stats['last_updated']
if current_age < timedelta(minutes=1):
current_age_text = 'Stats were updated less than a minute ago.'
elif current_age < timedelta(minutes=2):
current_age_text = 'Stats were updated one minute ago.'
elif current_age < timedelta(minutes=60):
minutes = current_age.seconds / 60
current_age_text = 'Stats were updated {:.0f} minutes ago.'.format(minutes)
else:
current_age_text = 'Stats were updated more than an hour ago'
data = {'stats': stats, 'current_age': current_age, 'current_age_text': current_age_text} data = {'stats': stats, 'current_age': current_age, 'current_age_text': current_age_text}
return render(request, 'stats/stats.html', data, status=status) return render(request, 'stats/stats.html', data, status=status)
def user_stats(request):
stats = caches['default'].get('userstats_{}'.format(request.user.id))
status = 503
current_age = None
current_age_text = None
if stats:
status = 200
current_age_text = age_text(stats['last_updated'])
data = {'stats': stats, 'current_age': current_age, 'current_age_text': current_age_text}
return render(request, 'stats/user.html', data, status=status)
def age_text(last_updated):
current_age = datetime.now() - last_updated
minutes = (current_age.seconds % 3600) / 60
hours = current_age.seconds / 3600
minutestr = "minute" if minutes == 1 else "minutes"
hourstr = "hour" if hours == 1 else "hours"
if current_age < timedelta(hours=1):
return 'Stats were updated {:.0f} {} ago.'.format(minutes, minutestr)
else:
return 'Stats were updated {:.0f} {} and {:.0f} {} ago.'.format(
hours, hourstr, minutes, minutestr)