Merge branch 'merge-upstream'

This commit is contained in:
Daan Sprenkels
2017-10-05 18:07:37 +02:00
13 changed files with 301 additions and 99 deletions

View File

@ -0,0 +1,6 @@
from django.conf import settings
def global_settings(request):
return {'issues_url': settings.ISSUES_URL, 'contact_email': settings.CONTACT_EMAIL,
'merge_requests_url': settings.MERGE_REQUESTS_URL}

View File

@ -66,6 +66,7 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
'marietje.context_processors.global_settings',
], ],
}, },
}, },
@ -79,8 +80,12 @@ WSGI_APPLICATION = 'marietje.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', 'ENGINE': 'django.db.backends.mysql',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 'NAME': 'marietje',
'USER': 'marietje',
'PASSWORD': 'UYmINKUDXIfY1qudVhbr5UhJ61kVwZPA',
'HOST': 'localhost',
'PORT': '3306',
} }
} }
@ -150,3 +155,11 @@ MAX_MINUTES_IN_A_ROW = 20
# Time range (dependent on timezone specified) when MAX_MINUTES_IN_A_ROW is in effect. # Time range (dependent on timezone specified) when MAX_MINUTES_IN_A_ROW is in effect.
LIMIT_ALWAYS = False LIMIT_ALWAYS = False
LIMIT_HOURS = (12, 16) LIMIT_HOURS = (12, 16)
CONTACT_EMAIL = 'marietje@science.ru.nl'
STATS_TOP_COUNT = 50
STATS_REQUEST_IGNORE_USER_IDS = (51, 515)
ISSUES_URL = 'https://gitlab.science.ru.nl/Marietje/MarietjeDjango/issues'
MERGE_REQUESTS_URL = 'https://gitlab.science.ru.nl/Marietje/MarietjeDjango/merge_requests'

View File

@ -17,3 +17,7 @@
#queue-time-header { #queue-time-header {
cursor: pointer; cursor: pointer;
} }
footer {
text-align: center;
}

View File

@ -48,39 +48,39 @@ $(function () {
$('#cancel-request').click(function () { $('#cancel-request').click(function () {
hideRequestTable(); hideRequestTable();
}); });
$('.pagenum').change(function(){ $('.pagenum').change(function(){
getSongs(); getSongs();
}); });
$('#search-all, #search-uploader').change(function(){ $('#search-all, #search-uploader').change(function(){
$('.pagenum').val(1); $('.pagenum').val(1);
getSongs(); getSongs();
}); });
$('.pagesize').change(function(){ $('.pagesize').change(function(){
Cookies.set('pagesize', $(this).val()); Cookies.set('pagesize', $(this).val(), { expires: 365 });
$('.pagenum').val(1); $('.pagenum').val(1);
getSongs(); getSongs();
}); });
$('button.prev').click(function(){ $('button.prev').click(function(){
var pageNumSelect = $('.pagenum'); var pageNumSelect = $('.pagenum');
pageNumSelect.val(Math.max(parseInt(pageNumSelect.val()) - 1, 1)); pageNumSelect.val(Math.max(parseInt(pageNumSelect.val()) - 1, 1));
getSongs(); getSongs();
}); });
$('button.next').click(function(){ $('button.next').click(function(){
var pageNumSelect = $('.pagenum'); var pageNumSelect = $('.pagenum');
pageNumSelect.val(Math.min(parseInt(pageNumSelect.val()) + 1, pageNumSelect.children('option:last-child').val())); pageNumSelect.val(Math.min(parseInt(pageNumSelect.val()) + 1, pageNumSelect.children('option:last-child').val()));
getSongs(); getSongs();
}); });
$('button.first').click(function(){ $('button.first').click(function(){
$('.pagenum').val(1); $('.pagenum').val(1);
getSongs(); getSongs();
}); });
$('button.last').click(function(){ $('button.last').click(function(){
var pageNumSelect = $('.pagenum'); var pageNumSelect = $('.pagenum');
pageNumSelect.val(pageNumSelect.children('option:last-child').val()); pageNumSelect.val(pageNumSelect.children('option:last-child').val());
@ -126,7 +126,7 @@ $(function () {
$('#queue-time-header').click(function(){ $('#queue-time-header').click(function(){
showTimeToPlay = !showTimeToPlay; showTimeToPlay = !showTimeToPlay;
$('#queue-time-header').text(showTimeToPlay ? 'Plays In' : 'Plays At'); $('#queue-time-header').text(showTimeToPlay ? 'Plays In' : 'Plays At');
Cookies.set('showtimetoplay', showTimeToPlay ? '1' : '0'); Cookies.set('showtimetoplay', showTimeToPlay ? '1' : '0', { expires: 365 });
}); });
getSongs(); getSongs();
}); });
@ -279,6 +279,14 @@ function getSongs()
} }
pageNumSelect.val(result.current_page); pageNumSelect.val(result.current_page);
$('.pagesize').val(result.per_page); $('.pagesize').val(result.per_page);
$('button.first').prop('disabled', result.current_page == 1);
$('button.prev').prop('disabled', result.current_page == 1);
$('button.next').prop('disabled', result.current_page == result.last_page);
$('button.last').prop('disabled', result.current_page == result.last_page);
refreshingSongs = false;
if(requestViewOpen) {
window.scrollTo(0, 0);
}
}); });
} }

View File

@ -7,7 +7,7 @@
<title>Marietje 4.0 - {% block title %}{% endblock title %}</title> <title>Marietje 4.0 - {% block title %}{% endblock title %}</title>
<link rel="icon" type="image/x-icon" href="{% static 'favicon.ico' %}" /> <link rel="icon" type="image/x-icon" href="{% static 'favicon.ico' %}" />
<link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" /> <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}" />
<link rel="stylesheet" href="{% static 'css/custom.css' %}" /> <link rel="stylesheet" href="{% static 'css/custom.css' %}?2" />
<script type="text/javascript" src="{% static "js/jquery-3.1.1.min.js" %}"></script> <script type="text/javascript" src="{% static "js/jquery-3.1.1.min.js" %}"></script>
<script type="text/javascript" src="{% static "js/bootstrap.min.js" %}"></script> <script type="text/javascript" src="{% static "js/bootstrap.min.js" %}"></script>
</head> </head>
@ -59,7 +59,20 @@
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.extra_tags }} alert-dismissable">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button>
{{ message }}
</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock content %} {% block content %}{% endblock content %}
</div> </div>
<footer>
Issues? Missing a feature? Report them <a href="{{ issues_url }}" target="_blank">here</a>,
submit a merge request <a href="{{ merge_requests_url }}" target="_blank">here</a>
or send an email to <a href="mailto:{{ contact_email }}">{{ contact_email }}</a>.<br>
</footer>
</body> </body>
</html> </html>

View File

@ -1,11 +1,15 @@
import random, string import random
from django.shortcuts import render, redirect, get_object_or_404 import string
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.mail import send_mail from django.core.mail import send_mail
from django.conf import settings from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse from django.urls import reverse
from .forms import RegistrationForm, ResetPasswordForm
from marietje.utils import get_first_queue from marietje.utils import get_first_queue
from .forms import RegistrationForm, ResetPasswordForm
def register(request): def register(request):
@ -31,8 +35,9 @@ def register(request):
'Please confirm your account by following this link: ' + activation_link, 'Please confirm your account by following this link: ' + activation_link,
settings.MAIL_FROM, settings.MAIL_FROM,
[user.email], [user.email],
fail_silently=True fail_silently=True)
) messages.add_message(request, messages.INFO, 'Please check your email, ' + user.email + ', for activation.',
extra_tags='info')
return redirect('login') return redirect('login')
@ -42,6 +47,10 @@ def activate(request, user_id, token):
if token == user.activation_token: if token == user.activation_token:
user.activation_token = None user.activation_token = None
user.save() user.save()
messages.add_message(request, messages.SUCCESS, 'Successfully activated.', extra_tags='success')
else:
messages.add_message(request, messages.ERROR, 'Activation failed. If the problem persists, please contact '
+ settings.CONTACT_EMAIL + '.', extra_tags='danger')
return redirect('login') return redirect('login')
@ -52,6 +61,7 @@ def forgotpassword(request):
User = get_user_model() User = get_user_model()
user = User.objects.filter(username=request.POST.get('email')).first() user = User.objects.filter(username=request.POST.get('email')).first()
if user is None or user.activation_token: if user is None or user.activation_token:
messages.add_message(request, messages.ERROR, 'No (active) user found.', extra_tags='danger')
return render(request, 'registration/forgotpassword.html') return render(request, 'registration/forgotpassword.html')
user.reset_token = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32)) user.reset_token = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(32))
@ -63,8 +73,9 @@ def forgotpassword(request):
+ reset_link + '\nIf you did not request to reset your password, you can ignore this email.', + reset_link + '\nIf you did not request to reset your password, you can ignore this email.',
settings.MAIL_FROM, settings.MAIL_FROM,
[user.email], [user.email],
fail_silently=True fail_silently=True)
) messages.add_message(request, messages.INFO, 'Please check your email, ' + user.email
+ ', for resetting your password.', extra_tags='info')
return redirect('login') return redirect('login')
@ -72,6 +83,7 @@ def resetpassword(request, user_id, token):
User = get_user_model() User = get_user_model()
user = get_object_or_404(User, pk=user_id) user = get_object_or_404(User, pk=user_id)
if not user.reset_token or token != user.reset_token: if not user.reset_token or token != user.reset_token:
messages.add_message(request, messages.ERROR, 'Invalid password reset link.', extra_tags='danger')
return redirect('login') return redirect('login')
if not request.POST: if not request.POST:
return render(request, 'registration/resetpassword.html', {'user_id': user.id, 'reset_token': token}) return render(request, 'registration/resetpassword.html', {'user_id': user.id, 'reset_token': token})
@ -79,9 +91,11 @@ def resetpassword(request, user_id, token):
form = ResetPasswordForm(request.POST) form = ResetPasswordForm(request.POST)
if not form.is_valid(): if not form.is_valid():
return render(request, 'registration/resetpassword.html', {'user_id': user.id, 'reset_token': token, 'form': form}) return render(request, 'registration/resetpassword.html', {'user_id': user.id, 'reset_token': token,
'form': form})
user.reset_token = None user.reset_token = None
user.set_password(form.cleaned_data['password1']) user.set_password(form.cleaned_data['password1'])
user.save() user.save()
messages.add_message(request, messages.SUCCESS, 'Your password has been reset.', extra_tags='success')
return redirect('login') return redirect('login')

View File

@ -34,6 +34,10 @@ def play(request):
queue = get_object_or_404(Queue, id=request.POST.get('queue')) queue = get_object_or_404(Queue, id=request.POST.get('queue'))
queue.started_at = timezone.now() queue.started_at = timezone.now()
queue.save() queue.save()
current_song = queue.current_song()
current_song.played_at = queue.started_at
current_song.save()
return JsonResponse({}) return JsonResponse({})
@ -46,14 +50,6 @@ def next(request):
player_song.save() player_song.save()
return JsonResponse({}) return JsonResponse({})
@csrf_exempt
@token_required
def next(request):
queue = get_object_or_404(Queue, id=request.POST.get('queue'))
player_song = queue.current_song()
player_song.state = 2
player_song.save()
return JsonResponse({})
@csrf_exempt @csrf_exempt
@token_required @token_required

View File

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-09-23 22:59
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('queues', '0003_queuecommand'),
]
operations = [
migrations.RemoveField(
model_name='playlistsong',
name='order',
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-09-26 13:56
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('queues', '0004_remove_playlistsong_order'),
]
operations = [
migrations.AddField(
model_name='playlistsong',
name='played_at',
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -29,6 +29,7 @@ class PlaylistSong(models.Model):
blank=True, blank=True,
null=True null=True
) )
played_at = models.DateTimeField(blank=True, null=True)
# 0: Queued. # 0: Queued.
# 1: Playing. # 1: Playing.
@ -36,22 +37,20 @@ class PlaylistSong(models.Model):
# 3: Cancelled. # 3: Cancelled.
state = models.IntegerField(default=0) state = models.IntegerField(default=0)
order = models.IntegerField()
def move_up(self): def move_up(self):
other_song = PlaylistSong.objects.filter(playlist=self.playlist, order__lt=self.order)\ other_song = PlaylistSong.objects.filter(playlist=self.playlist, id__lt=self.id)\
.order_by('-order').first() .order_by('-id').first()
self.switch_order(other_song) self.switch_order(other_song)
def move_down(self): def move_down(self):
other_song = PlaylistSong.objects.filter(playlist=self.playlist, order__gt=self.order).order_by('order').first() other_song = PlaylistSong.objects.filter(playlist=self.playlist, id__gt=self.id).order_by('id').first()
self.switch_order(other_song) self.switch_order(other_song)
def switch_order(self, other_song): def switch_order(self, other_song):
old_order = self.order old_id = self.id
self.order = other_song.order self.id = other_song.id
other_song.id = old_id
self.save() self.save()
other_song.order = old_order
other_song.save() other_song.save()
def __str__(self): def __str__(self):
@ -91,7 +90,7 @@ class Queue(models.Model):
self.songs = PlaylistSong.objects\ self.songs = PlaylistSong.objects\
.filter(Q(playlist=self.playlist_id) | Q(playlist_id=self.random_playlist_id), .filter(Q(playlist=self.playlist_id) | Q(playlist_id=self.random_playlist_id),
Q(state=0) | Q(state=1))\ Q(state=0) | Q(state=1))\
.order_by('-state', 'playlist_id', 'order')\ .order_by('-state', 'playlist_id', 'id')\
.select_related('song', 'user') .select_related('song', 'user')
return self.songs return self.songs
@ -112,12 +111,10 @@ class Queue(models.Model):
return songs[1:] return songs[1:]
def request(self, song, user): def request(self, song, user):
playlist_songs = PlaylistSong.objects.filter(playlist=self.playlist, state=0).order_by('order') playlist_songs = PlaylistSong.objects.filter(playlist=self.playlist, state=0).order_by('id')
order = 0
seconds_in_a_row = 0 seconds_in_a_row = 0
for playlist_song in playlist_songs: for playlist_song in playlist_songs:
order = playlist_song.order
if playlist_song.user != user: if playlist_song.user != user:
seconds_in_a_row = 0 seconds_in_a_row = 0
else: else:
@ -129,27 +126,17 @@ class Queue(models.Model):
and not user.is_superuser and settings.LIMIT_HOURS[0] <= now.hour < settings.LIMIT_HOURS[1]: and not user.is_superuser and settings.LIMIT_HOURS[0] <= now.hour < settings.LIMIT_HOURS[1]:
return False return False
if order is None: playlist_song = PlaylistSong(playlist=self.playlist, song=song, user=user)
order = 0
order += 1
playlist_song = PlaylistSong(playlist=self.playlist, song=song, user=user, order=order)
playlist_song.save() playlist_song.save()
return True return True
def fill_random_queue(self): def fill_random_queue(self):
song_count = PlaylistSong.objects.filter(playlist_id=self.random_playlist_id, state=0).count() song_count = PlaylistSong.objects.filter(playlist_id=self.random_playlist_id, state=0).count()
while song_count < 5: while song_count < 5:
order = PlaylistSong.objects.filter(playlist_id=self.random_playlist_id)\
.aggregate(Max('order'))['order__max']
if order is None:
order = 0
order += 1
song = Song.objects.filter(deleted=False).order_by('?').first() song = Song.objects.filter(deleted=False).order_by('?').first()
if song is None: if song is None:
return return
playlist_song = PlaylistSong(playlist=self.random_playlist, playlist_song = PlaylistSong(playlist=self.random_playlist, song=song, user=None)
song=song,
user=None, order=order)
playlist_song.save() playlist_song.save()
song_count += 1 song_count += 1

View File

@ -88,7 +88,7 @@
</div> </div>
<script type="text/javascript" src="{% static 'js/js.cookie-2.1.3.min.js' %}"></script> <script type="text/javascript" src="{% static 'js/js.cookie-2.1.3.min.js' %}"></script>
<script type="text/javascript" src="{% static 'js/queue.js' %}"></script> <script type="text/javascript" src="{% static 'js/queue.js' %}?1"></script>
<script type="text/javascript"> <script type="text/javascript">
var csrf_token = "{{ csrf_token }}"; var csrf_token = "{{ csrf_token }}";
var canSkip = {{ perms.queues.can_skip|yesno:"1,0" }}; var canSkip = {{ perms.queues.can_skip|yesno:"1,0" }};

View File

@ -5,46 +5,132 @@
{% block content %} {% block content %}
<h1>Statistics</h1> <h1>Statistics</h1>
<h2>Uploads</h2> <div class="row">
<div class="table-responsive"> <div class="col-md-6">
<table class="table table-striped"> <h2>Uploads</h2>
<thead> <h4>Total: {{ total_uploads }}</h4>
<tr> <h4>Top {{ stats_top_count }}:</h4>
<th>#</th> <div class="table-responsive">
<th>User</th> <table class="table table-striped">
<th># Songs</th> <thead>
</tr> <tr>
</thead> <th>#</th>
<tbody> <th>User</th>
{% for stat in upload_stats %} <th>#&nbsp;Songs</th>
<tr> </tr>
<th>{{ forloop.counter }}</th> </thead>
<td>{{ stat.user__name }}</td> <tbody>
<td>{{ stat.total }}</td> {% for stat in upload_stats %}
</tr> <tr>
{% endfor %} <th>{{ forloop.counter }}</th>
</tbody> <td>{{ stat.user__name }}</td>
</table> <td>{{ stat.total }} ({% widthratio stat.total total_uploads 100 %}%)</td>
</div> </tr>
<h2>Requests</h2> {% endfor %}
<div class="table-responsive"> </tbody>
<table class="table table-striped"> </table>
<thead> </div>
<tr> </div>
<th>#</th> <div class="col-md-6">
<th>User</th> <h2>Requests</h2>
<th># Requests</th> <h4>Total: {{ total_requests }}</h4>
</tr> <h4>Top {{ stats_top_count }}:</h4>
</thead> <div class="table-responsive">
<tbody> <table class="table table-striped">
{% for stat in request_stats %} <thead>
<tr> <tr>
<th>{{ forloop.counter }}</th> <th>#</th>
<td>{{ stat.user__name }}</td> <th>User</th>
<td>{{ stat.total }}</td> <th>#&nbsp;Requests</th>
</tr> </tr>
{% endfor %} </thead>
</tbody> <tbody>
</table> {% for stat in request_stats %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.user__name }}</td>
<td>{{ stat.total }} ({% widthratio stat.total total_requests 100 %}%)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Unique requests</h2>
<h4>Top {{ stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>User</th>
<th>#&nbsp;Requests</th>
</tr>
</thead>
<tbody>
{% for stat in unique_request_stats %}
<tr>
<th>{{ forloop.counter }}</th>
<td>{{ stat.user__name }}</td>
<td>{{ stat.total }} ({{stat.ratio }}%)</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-md-6">
<h2>Most played songs</h2>
<h4>Top {{ stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th>#&nbsp;Requests</th>
</tr>
</thead>
<tbody>
{% for stat in 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="col-md-6">
<h2>Most played songs last 14 days</h2>
<h4>Top {{ stats_top_count }}:</h4>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Artist</th>
<th>Title</th>
<th>#&nbsp;Requests</th>
</tr>
</thead>
<tbody>
{% for stat in most_played_songs_14_days %}
<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> </div>
{% endblock %} {% endblock %}

View File

@ -1,10 +1,46 @@
from datetime import timedelta
from django.conf import settings
from django.db.models import Count, Q
from django.shortcuts import render from django.shortcuts import render
from django.db.models import Count from django.utils import timezone
from songs.models import Song
from queues.models import PlaylistSong from queues.models import PlaylistSong
from songs.models import Song
def stats(request): def stats(request):
upload_stats = Song.objects.all().exclude(user_id=None).values('user__name').annotate(total=Count('id')).order_by('-total') total_uploads = Song.objects.filter(deleted=False).exclude(user_id=None).count()
request_stats = PlaylistSong.objects.all().exclude(user_id=None).values('user__name').annotate(total=Count('id')).order_by('-total')
return render(request, 'stats/stats.html', {'upload_stats': upload_stats, 'request_stats': request_stats}) upload_stats = Song.objects.filter(deleted=False).exclude(user_id=None).values('user__name').annotate(
total=Count('id')).order_by('-total', 'user__name')[:settings.STATS_TOP_COUNT]
total_requests = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).count()
request_stats = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values('user__name').annotate(
total=Count('id')).order_by('-total', 'user__name')[:settings.STATS_TOP_COUNT]
unique_request_stats = PlaylistSong.objects.filter(state=2).exclude(
Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values(
'user__name', 'user__name').annotate(
total=Count('song_id', distinct=True), ratio=Count('song_id', distinct=True) / Count('id') * 100).order_by(
'-total')[:settings.STATS_TOP_COUNT]
most_played_songs = PlaylistSong.objects.filter(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')[:settings.STATS_TOP_COUNT]
most_played_songs_14_days = PlaylistSong.objects.filter(
state=2, played_at__gte=timezone.now() - timedelta(days=14)).exclude(user_id=None).values(
'song__artist', 'song__title').annotate(total=Count('id')).order_by(
'-total', 'song__artist')[:settings.STATS_TOP_COUNT]
return render(request, 'stats/stats.html', {'total_uploads': total_uploads, 'upload_stats': upload_stats,
'total_requests': total_requests, 'request_stats': request_stats,
'unique_request_stats': unique_request_stats,
'most_played_songs': most_played_songs,
'most_played_songs_14_days': most_played_songs_14_days,
'stats_top_count': settings.STATS_TOP_COUNT})