From e7ef899bb3df55b19f48208c884f4265f7f07003 Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 19:04:37 +0100 Subject: [PATCH 01/19] Improving alert box Fixed error with the cancel button and upgraded the returned messages --- marietje/marietje/static/js/queue.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/marietje/marietje/static/js/queue.js b/marietje/marietje/static/js/queue.js index 1d6e209..e34df2b 100644 --- a/marietje/marietje/static/js/queue.js +++ b/marietje/marietje/static/js/queue.js @@ -49,13 +49,16 @@ $(function () { var songId = $(this).data('report-song-id'); var message = prompt("What is wrong with the song?"); if (message == "") { - alert("Please enter a message."); + createAlert('danger', 'Please enter a message.'); return false } + if (message == null) { + 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!"); + createAlert('success', 'Thanks for your song report!'); } }); return false; From a2937bad710cff7edbc714246211bc86bfefdb38 Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 20:34:36 +0100 Subject: [PATCH 02/19] added usercache --- marietje/stats/utils.py | 4 ++-- marietje/stats/views.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index a4cae4b..9cd17e7 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -23,8 +23,8 @@ def recache_user_stats(): 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) + caches['userstats'].delete(cacheloc) + caches['userstats'].set(cacheloc, new_stats, 48 * 3600) return new_stats def to_days(time): diff --git a/marietje/stats/views.py b/marietje/stats/views.py index 232de2a..c3acf33 100644 --- a/marietje/stats/views.py +++ b/marietje/stats/views.py @@ -20,7 +20,7 @@ def stats(request): return render(request, 'stats/stats.html', data, status=status) def user_stats(request): - stats = caches['default'].get('userstats_{}'.format(request.user.id)) + stats = caches['userstats'].get('userstats_{}'.format(request.user.id)) status = 503 current_age = None current_age_text = None From e719771d1c002b49e249fda8c0f0e713823294f8 Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 20:36:24 +0100 Subject: [PATCH 03/19] added cache to settings file --- marietje/marietje/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/marietje/marietje/settings.py b/marietje/marietje/settings.py index fa3a15f..5d83da5 100644 --- a/marietje/marietje/settings.py +++ b/marietje/marietje/settings.py @@ -102,6 +102,11 @@ CACHES = { 'LOCATION': '/var/tmp/MarietjeDjango_cache/song_search', 'OPTIONS': { 'MAX_ENTRIES': 1000 }, }, + 'userstats': { + 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', + 'LOCATION': '/var/tmp/MarietjeDjango_cache/default', + 'OPTIONS': { 'MAX_ENTRIES': 1500 }, + }, } AUTH_USER_MODEL = 'marietje.User' From 264ec991e5718f6bd4cd8685938062486e219968 Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 20:48:52 +0100 Subject: [PATCH 04/19] changed Most played uploads to exclude your requests, and some small fixes --- marietje/stats/templates/stats/user.html | 15 +++++++++++---- marietje/stats/utils.py | 15 +++++++-------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/marietje/stats/templates/stats/user.html b/marietje/stats/templates/stats/user.html index 9f23409..f0bff7d 100644 --- a/marietje/stats/templates/stats/user.html +++ b/marietje/stats/templates/stats/user.html @@ -8,17 +8,22 @@

User Statistics

+ {% if not stats %} +
+ Stats unavailable :( +
+ {% else %} {% if current_age_text %}
{{ current_age_text }} {% endif %} +

Total uploads: {{ stats.total_uploads }}

-

Total requests: {{ stats.total_requests }}

Unique requests: {{ stats.unique_requests }} ({% widthratio stats.unique_requests stats.total_requests 100 %}%)

-

Total requested uploads: {{stats.total_played_uploads}}

Most played songs

+

Total: {{ stats.total_requests }}

Top {{ stats.stats_top_count }}:

@@ -45,7 +50,8 @@
-

Most played uploads

+

Uploads requested by others

+

Total: {{stats.total_played_uploads}}

Top {{ stats.stats_top_count }}:

@@ -87,7 +93,7 @@ - + {% endfor %} @@ -95,5 +101,6 @@ + {% endif %} {% endblock %} \ No newline at end of file diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index 9cd17e7..79a7542 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -60,7 +60,7 @@ def compute_stats(): 'user__id', 'user__name').annotate( total_requests=Count('id', distinct=True), unique_requests=Count('song__id', distinct=True)).order_by( - '-total_requests')[:settings.STATS_TOP_COUNT] + '-unique_requests')[:settings.STATS_TOP_COUNT] total_unique_requests = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) @@ -145,16 +145,15 @@ def user_stats(request): '-total')[:settings.STATS_TOP_COUNT] 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( + state=2, song_id__in=Song.objects.filter(user__id=request)).exclude(Q(user__id=None)|Q(user__id=request)).values( 'song__artist', 'song__title').annotate(total=Count('id')).order_by( '-total', 'song__artist', 'song__title')[:settings.STATS_TOP_COUNT] - - total_played_uploads = most_played_uploads.aggregate(newtotal=Sum('total')) + most_played = list(most_played_uploads) + total_played_uploads = 0 + for x in most_played: + total_played_uploads += x['total'] return { 'last_updated': last_updated, @@ -165,6 +164,6 @@ def user_stats(request): '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'], + 'total_played_uploads': total_played_uploads, } From 6a86cf1c0a0f1feb901a2994bd35689e837b0bf9 Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 20:51:54 +0100 Subject: [PATCH 05/19] split uploads in 2 columns --- marietje/stats/templates/stats/user.html | 9 ++++++--- marietje/stats/utils.py | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/marietje/stats/templates/stats/user.html b/marietje/stats/templates/stats/user.html index f0bff7d..d6a877c 100644 --- a/marietje/stats/templates/stats/user.html +++ b/marietje/stats/templates/stats/user.html @@ -50,8 +50,9 @@
-

Uploads requested by others

-

Total: {{stats.total_played_uploads}}

+

Uploads requested

+

Total played by you: {{stats.total_played_user_uploads}}

+

Total played by others: {{stats.total_played_uploads}}

Top {{ stats.stats_top_count }}:

{{ forloop.counter }} {{ stat.song__user__name }}{{ stat.total }} ({% widthratio stat.total total_requests 100 %}%){{ stat.total }} ({% widthratio stat.total stats.total_requests 100 %}%)
@@ -60,7 +61,8 @@ - + + @@ -70,6 +72,7 @@ + {% endfor %} diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index 79a7542..08730c5 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -145,15 +145,18 @@ def user_stats(request): '-total')[:settings.STATS_TOP_COUNT] 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=request)).values( + state=2, song_id__in=Song.objects.filter(user__id=request)).exclude(user__id=None).values( 'song__artist', - 'song__title').annotate(total=Count('id')).order_by( + 'song__title').annotate(total=Count('id', filter=~Q(user__id=request)), user_total=Count('id', filter=Q(user__id=request))).order_by( '-total', 'song__artist', 'song__title')[:settings.STATS_TOP_COUNT] + most_played = list(most_played_uploads) total_played_uploads = 0 + total_played_user_uploads = 0 for x in most_played: total_played_uploads += x['total'] + total_played_user_uploads += x['user_total'] return { 'last_updated': last_updated, @@ -165,5 +168,6 @@ def user_stats(request): 'most_played_uploads': list(most_played_uploads), 'stats_top_count': settings.STATS_TOP_COUNT, 'total_played_uploads': total_played_uploads, + 'total_played_user_uploads': total_played_user_uploads, } From 274949c51970f19f872cb0e3ef53ceed9c250296 Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 20:53:11 +0100 Subject: [PATCH 06/19] added average song length to stats --- marietje/stats/templates/stats/stats.html | 6 ++++-- marietje/stats/utils.py | 9 ++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/marietje/stats/templates/stats/stats.html b/marietje/stats/templates/stats/stats.html index 4262d7e..ae73f43 100644 --- a/marietje/stats/templates/stats/stats.html +++ b/marietje/stats/templates/stats/stats.html @@ -76,7 +76,8 @@ - + + @@ -84,7 +85,8 @@ - + + {% endfor %} diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index 08730c5..258acfb 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -27,9 +27,12 @@ def recache_user_stats(): caches['userstats'].set(cacheloc, new_stats, 48 * 3600) return new_stats -def to_days(time): +def time_convert(time): for tr in time: tr['duration'] = str(round(tr['total'] / 86400, 2)) + ' days' + avg_dur_sec = tr['avg_dur']%60 + avg_dur_min = int((tr['avg_dur']-avg_dur_sec)/60) + tr['avg_dur'] = '{}:{}'.format(avg_dur_min, avg_dur_sec) return time def compute_stats(): @@ -84,7 +87,7 @@ def compute_stats(): time_requested = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) | 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'), avg_dur=Sum('song__duration')/Count('id')).order_by( '-total')[:settings.STATS_TOP_COUNT] total_time_requested = PlaylistSong.objects.all().filter(state=2).exclude( @@ -108,7 +111,7 @@ def compute_stats(): 'total_unique_requests': total_unique_requests, 'most_played_songs': list(most_played_songs), 'most_played_songs_14_days': list(most_played_songs_14_days), - 'time_requested': to_days(list(time_requested)), + 'time_requested': time_convert(list(time_requested)), 'total_time_requested': str(round(float(total_time_requested['total']) / 86400, 2)) + ' days', 'stats_top_count': settings.STATS_TOP_COUNT, 'most_requested_uploaders': list(most_requested_uploaders), From 1cf43783167fd6b23544cdf0c3bc3208d337d957 Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 20:56:37 +0100 Subject: [PATCH 07/19] added descriptions to stats and userstats --- marietje/stats/templates/stats/stats.html | 33 ++++--- marietje/stats/templates/stats/user.html | 15 ++-- marietje/stats/utils.py | 102 +++++++++++++++++----- 3 files changed, 106 insertions(+), 44 deletions(-) diff --git a/marietje/stats/templates/stats/stats.html b/marietje/stats/templates/stats/stats.html index ae73f43..f6ed1f6 100644 --- a/marietje/stats/templates/stats/stats.html +++ b/marietje/stats/templates/stats/stats.html @@ -18,8 +18,8 @@

Uploads

-

Total: {{ stats.total_uploads }}

-

Top {{ stats.stats_top_count }}:

+

In total {{ stats.total_uploads }} songs have been uploaded. + These are the {{ stats.stats_top_count }} people who have uploaded the most.

# Artist Title# RequestsOthersYou
{{ stat.song__artist }} {{ stat.song__title }} {{ stat.total }}{{ stat.user_total }}
# UserDurationDurationAverage
{{ forloop.counter }} {{ stat.user__name }}{{ stat.duration }}{{ stat.duration }}{{stat.avg_dur}}
@@ -43,8 +43,8 @@

Requests

-

Total: {{ stats.total_requests }}

-

Top {{ stats.stats_top_count }}:

+

In total {{ stats.total_requests }} songs have been requested. + These are the {{ stats.stats_top_count }} people who have requested the most songs.

@@ -68,8 +68,9 @@

Time requested

-

Total: {{stats.total_time_requested}}

-

Top {{ stats.stats_top_count }}:

+

In total {{ stats.total_time_requested }} of music have been requested, with an + average song length of {{ stats.total_average }}. + These are the {{ stats.stats_top_count }} people with the longest total time queued

@@ -95,8 +96,9 @@

Unique requests

-

Total: {{stats.total_unique_requests.total}}

-

Top {{ stats.stats_top_count }}:

+

In total {{stats.total_unique_requests.total}} different songs + have been requested. The {{ stats.stats_top_count }} people that have requested the largest number of + different songs are shown below.

@@ -120,7 +122,7 @@

Most played songs

-

Top {{ stats.stats_top_count }}:

+

These are the {{ stats.stats_top_count }} most played songs ever.

@@ -146,22 +148,25 @@

Most played uploaders

-

Top {{ stats.stats_top_count }}:

+

The left column shows the {{ stats.stats_top_count }} people whose songs are requested most often by other people + people. The right column shows how many times that person has queued his own songs.

- + + {% for stat in stats.most_requested_uploaders %} - - + + + {% endfor %} @@ -170,7 +175,7 @@

Most played songs last 14 days

-

Top {{ stats.stats_top_count }}:

+

These songs are played the {{ stats.stats_top_count }} most in the last two weeks.

# User# Songs# Others# Own
{{ forloop.counter }}{{ stat.song__user__name }}{{ stat.total }} ({% widthratio stat.total stats.total_requests 100 %}%){{ stat.name }}{{ stat.total }}{{ stat.own_total}}
diff --git a/marietje/stats/templates/stats/user.html b/marietje/stats/templates/stats/user.html index d6a877c..0b373a5 100644 --- a/marietje/stats/templates/stats/user.html +++ b/marietje/stats/templates/stats/user.html @@ -19,11 +19,11 @@ {% endif %} -

Total uploads: {{ stats.total_uploads }}

-

Unique requests: {{ stats.unique_requests }} ({% widthratio stats.unique_requests stats.total_requests 100 %}%)

Most played songs

-

Total: {{ stats.total_requests }}

+

You have requested {{ stats.unique_requests }} different + songs a total of {{ stats.total_requests }} times. This + means {% widthratio stats.unique_requests stats.total_requests 100 %}% of your requests have been unique.

Top {{ stats.stats_top_count }}:

@@ -51,8 +51,11 @@

Uploads requested

-

Total played by you: {{stats.total_played_user_uploads}}

-

Total played by others: {{stats.total_played_uploads}}

+

You have uploaded a total of {{stats.total_uploads }} songs. The left column + shows how many times these have been requested by other people. The right column shows + how many times you requested your own songs. In total your songs + have been queued {{stats.total_played_uploads }} times by others and + {{stats.total_played_user_uploads }} by yourself.

Top {{ stats.stats_top_count }}:

@@ -81,7 +84,7 @@

Most played uploaders

-

Top {{ stats.stats_top_count }}:

+

The people whose songs you have queued the most are:

diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index 258acfb..8c94525 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -16,9 +16,11 @@ def recache_stats(): caches['default'].delete('stats') caches['default'].set('stats', new_stats, 2 * 3600) return new_stats - + + def recache_user_stats(): - users = User.objects.exclude(Q(id=None) + 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']) @@ -27,13 +29,51 @@ def recache_user_stats(): caches['userstats'].set(cacheloc, new_stats, 48 * 3600) return new_stats + def time_convert(time): - for tr in time: - tr['duration'] = str(round(tr['total'] / 86400, 2)) + ' days' - avg_dur_sec = tr['avg_dur']%60 - avg_dur_min = int((tr['avg_dur']-avg_dur_sec)/60) - tr['avg_dur'] = '{}:{}'.format(avg_dur_min, avg_dur_sec) - return time + try: + for tr in time: + tr['duration'] = str(round(tr['total'] / 86400, 2)) + ' days' + avg_dur_sec = tr['avg_dur'] % 60 + avg_dur_min = int((tr['avg_dur'] - avg_dur_sec) / 60) + tr['avg_dur'] = '{}:{}'.format(avg_dur_min, avg_dur_sec) + return time + except: + avg_dur_sec = round(time % 60) + avg_dur_min = int(round((time - avg_dur_sec) / 60)) + return ('{} minutes and {} seconds'.format(avg_dur_min, avg_dur_sec)) + + +def calculate_uploaders(requests_uploader, most_requested_uploaders): + for x in requests_uploader: + a = len(most_requested_uploaders) + b = 0 + while b <= a: + if b == a: + adding_list_item(most_requested_uploaders, x) + elif x['song__user__id'] == most_requested_uploaders[b]['id']: + if x['song__user__name'] == x['user__name']: + most_requested_uploaders[b]['own_total'] = x['total'] + else: + most_requested_uploaders[b]['total'] += x['total'] + break + b += 1 + return + + +def adding_list_item(list, x): + list.append({ + 'id': x['song__user__id'], + 'name': x['song__user__name'], + 'total': 0, + 'own_total': 0 + }) + if x['song__user__id'] == x['user__id']: + list[-1]['own_total']: x['total'] + else: + list[-1]['_total']: x['total'] + return + def compute_stats(): # We want to grab the time now, because otherwise we would be reporting a minute too late @@ -78,28 +118,41 @@ def compute_stats(): '-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] + 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] time_requested = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values( - 'user__id', 'user__name').annotate(total=Sum('song__duration'), avg_dur=Sum('song__duration')/Count('id')).order_by( - '-total')[:settings.STATS_TOP_COUNT] + 'user__id', 'user__name').annotate( + total=Sum('song__duration'), + avg_dur=Sum('song__duration') / + Count('id')).order_by('-total')[:settings.STATS_TOP_COUNT] total_time_requested = PlaylistSong.objects.all().filter(state=2).exclude( Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).aggregate( total=Sum('song__duration')) - most_requested_uploaders = PlaylistSong.objects.filter(state=2).exclude( + total_time_overall = 0 + count = 0 + for x in list(time_requested): + total_time_overall += x['avg_dur'] + count += 1 + total_average = total_time_overall / count + total_average = time_convert(total_average) + + requests_uploader = 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] + 'song__user__id', 'song__user__name', 'user__name', + 'user__id').annotate(total=Count('song__user__name')) + + most_requested_uploaders = [] + calculate_uploaders(list(requests_uploader), most_requested_uploaders) return { 'last_updated': last_updated, @@ -117,6 +170,7 @@ def compute_stats(): 'most_requested_uploaders': list(most_requested_uploaders), } + def user_stats(request): last_updated = datetime.now() @@ -148,11 +202,12 @@ def user_stats(request): '-total')[:settings.STATS_TOP_COUNT] most_played_uploads = PlaylistSong.objects.filter( - state=2, song_id__in=Song.objects.filter(user__id=request)).exclude(user__id=None).values( - 'song__artist', - 'song__title').annotate(total=Count('id', filter=~Q(user__id=request)), user_total=Count('id', filter=Q(user__id=request))).order_by( - '-total', 'song__artist', - 'song__title')[:settings.STATS_TOP_COUNT] + state=2, song_id__in=Song.objects.filter(user__id=request)).exclude( + user__id=None).values('song__artist', 'song__title').annotate( + total=Count('id', filter=~Q(user__id=request)), + user_total=Count('id', filter=Q(user__id=request))).order_by( + '-total', 'song__artist', + 'song__title')[:settings.STATS_TOP_COUNT] most_played = list(most_played_uploads) total_played_uploads = 0 @@ -173,4 +228,3 @@ def user_stats(request): 'total_played_uploads': total_played_uploads, 'total_played_user_uploads': total_played_user_uploads, } - From 00d0a32ece4603b71847096410d240bfa07a497c Mon Sep 17 00:00:00 2001 From: oslomp Date: Thu, 10 Jan 2019 21:01:15 +0100 Subject: [PATCH 08/19] general clean-up --- marietje/stats/utils.py | 74 ++++++++++++++++++----------------------- marietje/stats/views.py | 2 -- 2 files changed, 32 insertions(+), 44 deletions(-) diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index 8c94525..5a276b7 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -1,9 +1,8 @@ -from datetime import datetime, timedelta, time +from datetime import datetime, timedelta from django.core.cache import caches from django.conf import settings from django.db.models import Count, Q, Sum -from django.shortcuts import render from django.utils import timezone from queues.models import PlaylistSong @@ -30,49 +29,34 @@ def recache_user_stats(): return new_stats -def time_convert(time): - try: - for tr in time: - tr['duration'] = str(round(tr['total'] / 86400, 2)) + ' days' - avg_dur_sec = tr['avg_dur'] % 60 - avg_dur_min = int((tr['avg_dur'] - avg_dur_sec) / 60) - tr['avg_dur'] = '{}:{}'.format(avg_dur_min, avg_dur_sec) - return time - except: - avg_dur_sec = round(time % 60) - avg_dur_min = int(round((time - avg_dur_sec) / 60)) - return ('{} minutes and {} seconds'.format(avg_dur_min, avg_dur_sec)) - - -def calculate_uploaders(requests_uploader, most_requested_uploaders): - for x in requests_uploader: +def best_uploaders_list(requests_uploader, most_requested_uploaders): + for requests in requests_uploader: a = len(most_requested_uploaders) b = 0 while b <= a: if b == a: - adding_list_item(most_requested_uploaders, x) - elif x['song__user__id'] == most_requested_uploaders[b]['id']: - if x['song__user__name'] == x['user__name']: - most_requested_uploaders[b]['own_total'] = x['total'] + adding_list_item(most_requested_uploaders, requests) + elif requests['song__user__id'] == most_requested_uploaders[b]['id']: + if requests['song__user__name'] == requests['user__name']: + most_requested_uploaders[b]['own_total'] = requests['total'] else: - most_requested_uploaders[b]['total'] += x['total'] + most_requested_uploaders[b]['total'] += requests['total'] break b += 1 - return -def adding_list_item(list, x): - list.append({ - 'id': x['song__user__id'], - 'name': x['song__user__name'], +def adding_list_item(most_requested_list, requests): + # adds a single item to the best_uploaders list + most_requested_list.append({ + 'id': requests['song__user__id'], + 'name': requests['song__user__name'], 'total': 0, 'own_total': 0 }) - if x['song__user__id'] == x['user__id']: - list[-1]['own_total']: x['total'] + if requests['song__user__id'] == requests['user__id']: + most_requested_list[-1]['own_total']: requests['total'] else: - list[-1]['_total']: x['total'] - return + most_requested_list[-1]['_total']: requests['total'] def compute_stats(): @@ -137,13 +121,10 @@ def compute_stats(): | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).aggregate( total=Sum('song__duration')) - total_time_overall = 0 - count = 0 - for x in list(time_requested): - total_time_overall += x['avg_dur'] - count += 1 - total_average = total_time_overall / count - total_average = time_convert(total_average) + total_time_overall = sum(x['avg_dur'] for x in time_requested) + total_average = total_time_overall / len(time_requested) + avg_dur_min, avg_dur_sec = divmod(total_average, 60) + total_average = '{} minutes and {} seconds'.format(avg_dur_min, avg_dur_sec) requests_uploader = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) @@ -152,7 +133,15 @@ def compute_stats(): 'user__id').annotate(total=Count('song__user__name')) most_requested_uploaders = [] - calculate_uploaders(list(requests_uploader), most_requested_uploaders) + best_uploaders_list(list(requests_uploader), most_requested_uploaders) + + for time in list(time_requested): + # converts total time and average time in respectively days and minutes, seconds + time['duration'] = str(round(time['total'] / 86400, 2)) + ' days' + avg_dur_min, avg_dur_sec = divmod(time['avg_dur'], 60) + if avg_dur_sec < 10: + avg_dur_sec = '0' + str(avg_dur_sec) + time['avg_dur'] = '{}:{}'.format(avg_dur_min, avg_dur_sec) return { 'last_updated': last_updated, @@ -164,10 +153,11 @@ def compute_stats(): 'total_unique_requests': total_unique_requests, 'most_played_songs': list(most_played_songs), 'most_played_songs_14_days': list(most_played_songs_14_days), - 'time_requested': time_convert(list(time_requested)), + 'time_requested': time_requested, 'total_time_requested': str(round(float(total_time_requested['total']) / 86400, 2)) + ' days', 'stats_top_count': settings.STATS_TOP_COUNT, 'most_requested_uploaders': list(most_requested_uploaders), + 'total_average': total_average, } @@ -227,4 +217,4 @@ def user_stats(request): 'stats_top_count': settings.STATS_TOP_COUNT, 'total_played_uploads': total_played_uploads, 'total_played_user_uploads': total_played_user_uploads, - } + } \ No newline at end of file diff --git a/marietje/stats/views.py b/marietje/stats/views.py index c3acf33..ad60a29 100644 --- a/marietje/stats/views.py +++ b/marietje/stats/views.py @@ -2,8 +2,6 @@ from datetime import datetime, timedelta from django.core.cache import caches from django.shortcuts import render -from stats.utils import user_stats -from django.http import JsonResponse, HttpResponseForbidden def stats(request): From 585485b130080c11003fc4cecfb3cb6d018d05ed Mon Sep 17 00:00:00 2001 From: oslomp Date: Fri, 11 Jan 2019 13:11:25 +0100 Subject: [PATCH 09/19] Visually separate regular and Marietje's queue Original commit message: Arrows and marietje part of queue updates --- marietje/api/urls.py | 1 - marietje/api/views.py | 10 ------- marietje/marietje/static/css/custom.css | 8 ++++++ marietje/marietje/static/js/queue.js | 37 ++++++++++++++++++++++--- marietje/queues/models.py | 9 +----- 5 files changed, 42 insertions(+), 23 deletions(-) diff --git a/marietje/api/urls.py b/marietje/api/urls.py index ac0ad39..8fe583d 100644 --- a/marietje/api/urls.py +++ b/marietje/api/urls.py @@ -13,7 +13,6 @@ urlpatterns = [ url(r'^request', views.request), url(r'^report', views.report), url(r'^skip', views.skip), - url(r'^moveup', views.move_up), url(r'^movedown', views.move_down), url(r'^cancel', views.cancel), url(r'^upload', views.upload), diff --git a/marietje/api/views.py b/marietje/api/views.py index 30e9871..7a21351 100644 --- a/marietje/api/views.py +++ b/marietje/api/views.py @@ -188,16 +188,6 @@ def skip(request): return JsonResponse({}) -@require_http_methods(["POST"]) -@api_auth_required -def move_up(request): - if not request.user.has_perm('queues.can_move'): - return HttpResponseForbidden() - playlist_song = get_object_or_404(PlaylistSong, id=request.POST.get('id')) - playlist_song.move_up() - return JsonResponse({}) - - @require_http_methods(["POST"]) @api_auth_required def move_down(request): diff --git a/marietje/marietje/static/css/custom.css b/marietje/marietje/static/css/custom.css index f97451c..c9e8d79 100644 --- a/marietje/marietje/static/css/custom.css +++ b/marietje/marietje/static/css/custom.css @@ -21,3 +21,11 @@ footer { text-align: center; } + +.marietjequeue { + color: #777777; +} + +.marietjequeue-start { + border-top: 4px double #777777; +} \ No newline at end of file diff --git a/marietje/marietje/static/js/queue.js b/marietje/marietje/static/js/queue.js index 1d6e209..431574b 100644 --- a/marietje/marietje/static/js/queue.js +++ b/marietje/marietje/static/js/queue.js @@ -194,27 +194,56 @@ function renderQueue(playNextAt, now) { $('.queuebody').empty(); var timeToPlay = playNextAt - now; + var canDeletePrevious = false; $.each(queue, function (id, song) { var requestedBy = song.requested_by; + var reqMarietje = requestedBy != 'Marietje'; + var startMarietje = false + + //checks if id is the last item and returns false if the next song is Marietje, while the current song is not. + if(id === queue.length-1){ + var requestNext = false + } else { + var requestNext = !((queue[id+1].requested_by === 'Marietje') && (requestedBy !== 'Marietje')) + } + //checks if id is the first item and returns false if the previous song is not Marietje, while the current song is. + if(id === 0){ + var requestPrev = false + } else { + var prevItem = queue[id-1].id + if(queue[id-1].requested_by !== 'Marietje'){ + var requestPrev = false + if (requestedBy == 'Marietje'){ + var startMarietje = true + } else { + var requestPrev = true + } + } else {var requestPrev = true} + } + var canDelete = song.can_move_down || canMoveSongs; - var canMoveDown = canDelete && id < queue.length - 1; + var canMoveUp = canMoveSongs && requestPrev || canDeletePrevious && reqMarietje && requestPrev; + var canMoveDown = canMoveSongs && requestNext || canDelete && reqMarietje && requestNext; var artist = song.song.artist.trim() === '' ? '?' : song.song.artist; var title = song.song.title.trim() === '' ? '?' : song.song.title; + var marietjeclass = reqMarietje ? '' : ' class="marietjequeue"'; + var marietjestartclass = startMarietje ? ' class="marietjequeue marietjequeue-start"' : ''; showTime = showTimeToPlay ? (timeToPlay < 0 ? '' : timeToPlay.secondsToMMSS()) : (playNextAt < now ? '' : playNextAt.timestampToHHMMSS()) - $('.queuebody:last-child').append(''); timeToPlay += parseInt(song.song.duration); + canDeletePrevious = canDelete if(playNextAt >= now) { playNextAt += parseInt(song.song.duration); diff --git a/marietje/queues/models.py b/marietje/queues/models.py index 7f765eb..b6fbfef 100644 --- a/marietje/queues/models.py +++ b/marietje/queues/models.py @@ -47,16 +47,9 @@ class PlaylistSong(models.Model): ) state = models.IntegerField(default=0, db_index=True, choices=STATECHOICE) - def move_up(self): - other_song = PlaylistSong.objects.filter(playlist=self.playlist, id__lt=self.id)\ - .order_by('-id').first() - self.switch_order(other_song) def move_down(self): - other_song = PlaylistSong.objects.filter(playlist=self.playlist, id__gt=self.id).order_by('id').first() - self.switch_order(other_song) - - def switch_order(self, other_song): + other_song = PlaylistSong.objects.filter(playlist=self.playlist, id__gt=self.id).first() old_id = self.id self.id = other_song.id other_song.id = old_id From 12ad8135bd7766eb58946f49337917558552c478 Mon Sep 17 00:00:00 2001 From: oslomp Date: Wed, 16 Jan 2019 21:49:55 +0100 Subject: [PATCH 10/19] fix query not being listed --- marietje/stats/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index 5a276b7..8038090 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -121,7 +121,7 @@ def compute_stats(): | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).aggregate( total=Sum('song__duration')) - total_time_overall = sum(x['avg_dur'] for x in time_requested) + total_time_overall = sum(x['avg_dur'] for x in list(time_requested)) total_average = total_time_overall / len(time_requested) avg_dur_min, avg_dur_sec = divmod(total_average, 60) total_average = '{} minutes and {} seconds'.format(avg_dur_min, avg_dur_sec) From 42a631641e72baa9dcc18d88744a3670b8511046 Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Wed, 16 Jan 2019 19:28:50 +0100 Subject: [PATCH 11/19] Add pylint stub --- .gitlab-ci.yml | 11 + pylintrc | 565 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 576 insertions(+) create mode 100644 .gitlab-ci.yml create mode 100644 pylintrc diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..cb31dad --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,11 @@ +before_script: + - apt-get -qq update + - apt-get -qq install -y python3 python3-venv python3-pip + - python3 -m venv venv + - source venv/bin/activate + - pip install pylint + +pylint: + script: + - pylint marietje metrics playerapi queues songs stats + allow_failure: yes diff --git a/pylintrc b/pylintrc new file mode 100644 index 0000000..8250f8b --- /dev/null +++ b/pylintrc @@ -0,0 +1,565 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=migrations + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=missing-docstring, + line-too-long, + no-member, + parameter-unpacking, + unpacking-in-except, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata, + blah, + bla + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=50 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + x, y, z, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception". +overgeneral-exceptions=Exception From f10a4759da6b1f147448c310cf1f9f69e4318eee Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Sun, 20 Jan 2019 22:27:03 +0100 Subject: [PATCH 12/19] Move issues/MRs urls to dsprenkels Fixes #1 --- marietje/marietje/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/marietje/marietje/settings.py b/marietje/marietje/settings.py index fa3a15f..07c5038 100644 --- a/marietje/marietje/settings.py +++ b/marietje/marietje/settings.py @@ -139,8 +139,8 @@ CONTACT_EMAIL = 'marietje@science.ru.nl' STATS_TOP_COUNT = 50 STATS_REQUEST_IGNORE_USER_IDS = [] -ISSUES_URL = 'https://gitlab.science.ru.nl/Marietje/MarietjeDjango/issues' -MERGE_REQUESTS_URL = 'https://gitlab.science.ru.nl/Marietje/MarietjeDjango/merge_requests' +ISSUES_URL = 'https://gitlab.science.ru.nl/dsprenkels/MarietjeDjango/issues' +MERGE_REQUESTS_URL = 'https://gitlab.science.ru.nl/dsprenkels/MarietjeDjango/merge_requests' TRUSTED_IP_RANGES = [ '131.174.0.0/16', # RU wired '145.116.136.0/22', # eduroam From 14bfa8a9a015d197470604d9b5f7f94c5d20d5ac Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Sun, 20 Jan 2019 22:31:34 +0100 Subject: [PATCH 13/19] ci: Install django --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index cb31dad..4862474 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,7 +3,7 @@ before_script: - apt-get -qq install -y python3 python3-venv python3-pip - python3 -m venv venv - source venv/bin/activate - - pip install pylint + - pip install -r requirements.txt pylint pylint: script: From 2c41e85753818f20f77a89e82320bf8e5330853e Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Sun, 20 Jan 2019 22:36:19 +0100 Subject: [PATCH 14/19] ci: Make pylint modules more explicit --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4862474..fab58b5 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,5 +7,5 @@ before_script: pylint: script: - - pylint marietje metrics playerapi queues songs stats + - pylint marietje/marietje marietje/metrics marietje/playerapi marietje/queues marietje/songs marietje/stats allow_failure: yes From 19c1c70cd300b1bb4e77508846a4c049c8a04917 Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Mon, 21 Jan 2019 22:26:43 +0100 Subject: [PATCH 15/19] fix all pylint complaints --- .gitlab-ci.yml | 1 - marietje/api/views.py | 4 ++-- marietje/marietje/forms.py | 3 --- marietje/marietje/models.py | 6 +++--- marietje/marietje/settings.py | 6 +++--- marietje/marietje/utils.py | 29 ++++++++++++++++------------- marietje/marietje/views.py | 3 +-- marietje/metrics/admin.py | 3 --- marietje/metrics/models.py | 3 --- marietje/metrics/tests.py | 3 --- marietje/metrics/views.py | 10 ++++------ marietje/playerapi/admin.py | 3 --- marietje/playerapi/models.py | 3 --- marietje/playerapi/tests.py | 3 --- marietje/playerapi/views.py | 19 ++++++++++--------- marietje/queues/models.py | 9 ++++----- marietje/queues/tests.py | 3 --- marietje/queues/urls.py | 8 +------- marietje/songs/admin.py | 3 ++- marietje/songs/models.py | 7 +++++-- marietje/songs/tests.py | 3 --- marietje/songs/views.py | 6 +++--- marietje/stats/admin.py | 3 --- marietje/stats/models.py | 3 --- marietje/stats/tests.py | 3 --- marietje/stats/utils.py | 22 ++++++++++------------ marietje/stats/views.py | 23 ++++++++++------------- pylintrc | 12 +++++++++++- 28 files changed, 85 insertions(+), 119 deletions(-) delete mode 100644 marietje/metrics/admin.py delete mode 100644 marietje/metrics/models.py delete mode 100644 marietje/metrics/tests.py delete mode 100644 marietje/playerapi/admin.py delete mode 100644 marietje/playerapi/models.py delete mode 100644 marietje/playerapi/tests.py delete mode 100644 marietje/queues/tests.py delete mode 100644 marietje/songs/tests.py delete mode 100644 marietje/stats/admin.py delete mode 100644 marietje/stats/models.py delete mode 100644 marietje/stats/tests.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index fab58b5..25975d3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,4 +8,3 @@ before_script: pylint: script: - pylint marietje/marietje marietje/metrics marietje/playerapi marietje/queues marietje/songs marietje/stats - allow_failure: yes diff --git a/marietje/api/views.py b/marietje/api/views.py index 7a21351..ec0d2f0 100644 --- a/marietje/api/views.py +++ b/marietje/api/views.py @@ -111,7 +111,7 @@ def songs(request): except EmptyPage: songs = paginator.page(paginator.num_pages) - songs_dict = [song_to_dict(song, user=True) for song in songs.object_list] + songs_dict = [song_to_dict(song, include_user=True) for song in songs.object_list] return JsonResponse({ 'per_page': pagesize, 'current_page': page, @@ -170,7 +170,7 @@ def queue(request): queue = request.user.queue return JsonResponse({ 'current_song': playlist_song_to_dict(queue.current_song()), - 'queue': [playlist_song_to_dict(playlist_song, user=request.user) for playlist_song in queue.queue()], + 'queue': [playlist_song_to_dict(playlist_song, include_user=request.user) for playlist_song in queue.queue()], 'started_at': 0 if queue.started_at is None else int(queue.started_at.timestamp()), 'current_time': int(time.time()) }) diff --git a/marietje/marietje/forms.py b/marietje/marietje/forms.py index c04b1c1..41e741a 100644 --- a/marietje/marietje/forms.py +++ b/marietje/marietje/forms.py @@ -5,9 +5,6 @@ from django.utils.translation import ugettext_lazy as _ class AuthenticationForm(BaseAuthenticationForm): - def __init__(self, request=None, *args, **kwargs): - super(AuthenticationForm, self).__init__(request, *args, **kwargs) - def confirm_login_allowed(self, user): if user.activation_token: raise forms.ValidationError( diff --git a/marietje/marietje/models.py b/marietje/marietje/models.py index b89c502..6421c5d 100644 --- a/marietje/marietje/models.py +++ b/marietje/marietje/models.py @@ -1,14 +1,14 @@ -from django.db import models -from queues.models import Queue from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.contrib.auth.hashers import make_password from django.contrib.auth.models import PermissionsMixin from django.contrib.auth.validators import ASCIIUsernameValidator, UnicodeUsernameValidator +from django.core.mail import send_mail +from django.db import models from django.utils import six, timezone from django.utils.translation import ugettext_lazy as _ -from django.core.mail import send_mail from marietje.utils import get_first_queue +from queues.models import Queue class UserManager(BaseUserManager): diff --git a/marietje/marietje/settings.py b/marietje/marietje/settings.py index 07c5038..0f4fb17 100644 --- a/marietje/marietje/settings.py +++ b/marietje/marietje/settings.py @@ -62,7 +62,7 @@ DATABASES = { 'PASSWORD': 'v8TzZwdAdSi7Tk5I', 'HOST': 'localhost', 'PORT': '3306', - 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'" }, + 'OPTIONS': {'init_command': "SET sql_mode='STRICT_TRANS_TABLES'"}, } } @@ -95,12 +95,12 @@ CACHES = { 'default': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': '/var/tmp/MarietjeDjango_cache/default', - 'OPTIONS': { 'MAX_ENTRIES': 1000 }, + 'OPTIONS': {'MAX_ENTRIES': 1000}, }, 'song_search': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': '/var/tmp/MarietjeDjango_cache/song_search', - 'OPTIONS': { 'MAX_ENTRIES': 1000 }, + 'OPTIONS': {'MAX_ENTRIES': 1000}, }, } diff --git a/marietje/marietje/utils.py b/marietje/marietje/utils.py index 8c6f03b..b7fea32 100644 --- a/marietje/marietje/utils.py +++ b/marietje/marietje/utils.py @@ -1,10 +1,13 @@ -import socket, struct, binascii +import binascii +import socket +import struct + from django.conf import settings -from queues.models import Queue, Playlist from django.http import StreamingHttpResponse +from queues.models import Queue, Playlist -def song_to_dict(song, hash=False, user=False, replaygain=False): +def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False): data = { 'id': song.id, 'artist': song.artist, @@ -12,13 +15,13 @@ def song_to_dict(song, hash=False, user=False, replaygain=False): 'duration': song.duration, } - if hash: + if include_hash: data['hash'] = song.hash - if user is not None and song.user is not None and song.user.name: + if include_user is not None and song.user is not None and song.user.name: data['uploader_name'] = song.user.name - if replaygain: + if include_replaygain: data['rg_gain'] = song.rg_gain data['rg_peak'] = song.rg_peak @@ -44,9 +47,9 @@ def send_to_bertha(file): for chunk in file.chunks(): sock.sendall(chunk) sock.shutdown(socket.SHUT_WR) - hash = binascii.hexlify(sock.recv(64)) + song_hash = binascii.hexlify(sock.recv(64)) sock.close() - return hash + return song_hash def get_first_queue(): @@ -61,10 +64,10 @@ def get_first_queue(): return queue -def bertha_stream(hash): +def bertha_stream(song_hash): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(settings.BERTHA_HOST) - sock.sendall(struct.pack(" Date: Wed, 30 Jan 2019 13:37:59 +0100 Subject: [PATCH 16/19] Fix bug introduced by 19c1c70 --- marietje/api/views.py | 2 +- marietje/marietje/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/marietje/api/views.py b/marietje/api/views.py index ec0d2f0..5dc453e 100644 --- a/marietje/api/views.py +++ b/marietje/api/views.py @@ -170,7 +170,7 @@ def queue(request): queue = request.user.queue return JsonResponse({ 'current_song': playlist_song_to_dict(queue.current_song()), - 'queue': [playlist_song_to_dict(playlist_song, include_user=request.user) for playlist_song in queue.queue()], + 'queue': [playlist_song_to_dict(playlist_song, user=request.user) for playlist_song in queue.queue()], 'started_at': 0 if queue.started_at is None else int(queue.started_at.timestamp()), 'current_time': int(time.time()) }) diff --git a/marietje/marietje/utils.py b/marietje/marietje/utils.py index b7fea32..ae32c35 100644 --- a/marietje/marietje/utils.py +++ b/marietje/marietje/utils.py @@ -7,7 +7,7 @@ from django.http import StreamingHttpResponse from queues.models import Queue, Playlist -def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False): +def song_to_dict(song, include_hash=False, include_user=False, include_replaygain=False, **options): data = { 'id': song.id, 'artist': song.artist, From 4ac3d6e42575caff6a237078df5967e671b05b35 Mon Sep 17 00:00:00 2001 From: Daan Sprenkels Date: Wed, 30 Jan 2019 13:19:46 +0100 Subject: [PATCH 17/19] reports: Make songs readonly for performance --- marietje/songs/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/marietje/songs/admin.py b/marietje/songs/admin.py index 9ff8927..79f456a 100644 --- a/marietje/songs/admin.py +++ b/marietje/songs/admin.py @@ -32,3 +32,4 @@ class SongAdmin(admin.ModelAdmin): class ReportNoteAdmin(admin.ModelAdmin): list_display = ('song', 'note', 'user') search_fields = ('song__artist', 'song__title', 'user__name') + readonly_fields = ('song',) From 1b4108c5d04a62025a6dd2a5766bcd60bbe936a1 Mon Sep 17 00:00:00 2001 From: oslomp Date: Wed, 30 Jan 2019 16:47:43 +0100 Subject: [PATCH 18/19] fix indentations --- marietje/stats/utils.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index c24357d..0541b89 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -18,8 +18,9 @@ def recache_stats(): def recache_user_stats(): - users = User.objects.exclude(Q(id=None) - | Q(id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values('id') + 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']) @@ -35,9 +36,11 @@ def best_uploaders_list(requests_uploader, most_requested_uploaders): while b <= a: if b == a: adding_list_item(most_requested_uploaders, requests) - elif requests['song__user__id'] == most_requested_uploaders[b]['id']: + elif requests[ + 'song__user__id'] == most_requested_uploaders[b]['id']: if requests['song__user__name'] == requests['user__name']: - most_requested_uploaders[b]['own_total'] = requests['total'] + most_requested_uploaders[b][ + 'own_total'] = requests['total'] else: most_requested_uploaders[b]['total'] += requests['total'] break @@ -53,9 +56,9 @@ def adding_list_item(most_requested_list, requests): 'own_total': 0 }) if requests['song__user__id'] == requests['user__id']: - most_requested_list[-1]['own_total']: requests['total'] + most_requested_list[-1]['own_total'] = requests['total'] else: - most_requested_list[-1]['_total']: requests['total'] + most_requested_list[-1]['total'] = requests['total'] def compute_stats(): @@ -124,7 +127,8 @@ def compute_stats(): total_time_overall = sum(x['avg_dur'] for x in list(time_requested)) total_average = total_time_overall / len(time_requested) avg_dur_min, avg_dur_sec = divmod(total_average, 60) - total_average = '{} minutes and {} seconds'.format(avg_dur_min, avg_dur_sec) + total_average = '{} minutes and {} seconds'.format( + avg_dur_min, avg_dur_sec) requests_uploader = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) @@ -159,7 +163,8 @@ def compute_stats(): 'most_played_songs': list(most_played_songs), 'most_played_songs_14_days': list(most_played_songs_14_days), 'time_requested': 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, 'most_requested_uploaders': list(most_requested_uploaders), 'total_average': total_average, @@ -222,4 +227,4 @@ def user_stats(request): 'stats_top_count': settings.STATS_TOP_COUNT, 'total_played_uploads': total_played_uploads, 'total_played_user_uploads': total_played_user_uploads, - } \ No newline at end of file + } From 6dc36f2092aa1cf1033879567e91219f46e93306 Mon Sep 17 00:00:00 2001 From: oslomp Date: Mon, 4 Feb 2019 19:53:20 +0100 Subject: [PATCH 19/19] pylint fixes --- marietje/marietje/settings.py | 2 +- marietje/stats/utils.py | 52 +++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/marietje/marietje/settings.py b/marietje/marietje/settings.py index 3109ead..4745f32 100644 --- a/marietje/marietje/settings.py +++ b/marietje/marietje/settings.py @@ -105,7 +105,7 @@ CACHES = { 'userstats': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', 'LOCATION': '/var/tmp/MarietjeDjango_cache/default', - 'OPTIONS': { 'MAX_ENTRIES': 1500 }, + 'OPTIONS': {'MAX_ENTRIES': 1500}, }, } diff --git a/marietje/stats/utils.py b/marietje/stats/utils.py index 0541b89..5555ea1 100644 --- a/marietje/stats/utils.py +++ b/marietje/stats/utils.py @@ -64,26 +64,27 @@ def adding_list_item(most_requested_list, requests): def compute_stats(): # We want to grab the time now, because otherwise we would be reporting a minute too late last_updated = datetime.now() + stats = {} - total_uploads = Song.objects.filter(deleted=False).exclude( + stats['total_uploads'] = Song.objects.filter(deleted=False).exclude( user_id=None).count() - upload_stats = Song.objects.filter(deleted=False).exclude( + stats['upload_stats'] = Song.objects.filter(deleted=False).exclude( user_id=None).values( 'user__id', 'user__name').annotate(total=Count('id')).order_by( '-total', 'user__name')[:settings.STATS_TOP_COUNT] - total_requests = PlaylistSong.objects.filter(state=2).exclude( + stats['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( + stats['request_stats'] = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values( 'user__id', 'user__name').annotate(total=Count('id')).order_by( '-total', 'user__name')[:settings.STATS_TOP_COUNT] - unique_request_stats = PlaylistSong.objects.filter(state=2).exclude( + stats['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__id', 'user__name').annotate( @@ -91,27 +92,26 @@ def compute_stats(): unique_requests=Count('song__id', distinct=True)).order_by( '-unique_requests')[:settings.STATS_TOP_COUNT] - total_unique_requests = PlaylistSong.objects.filter(state=2).exclude( + stats['total_unique_requests'] = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) | 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( + stats['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( - + stats['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] - time_requested = PlaylistSong.objects.filter(state=2).exclude( + stats['time_requested'] = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).values( 'user__id', 'user__name').annotate( @@ -119,16 +119,16 @@ def compute_stats(): avg_dur=Sum('song__duration') / Count('id')).order_by('-total')[:settings.STATS_TOP_COUNT] - total_time_requested = PlaylistSong.objects.all().filter(state=2).exclude( + stats['total_time_requested'] = PlaylistSong.objects.all().filter(state=2).exclude( Q(user_id=None) | Q(user_id__in=settings.STATS_REQUEST_IGNORE_USER_IDS)).aggregate( total=Sum('song__duration')) - total_time_overall = sum(x['avg_dur'] for x in list(time_requested)) - total_average = total_time_overall / len(time_requested) + total_time_overall = sum(x['avg_dur'] for x in list(stats['time_requested'])) + total_average = total_time_overall / len(stats['time_requested']) avg_dur_min, avg_dur_sec = divmod(total_average, 60) total_average = '{} minutes and {} seconds'.format( - avg_dur_min, avg_dur_sec) + avg_dur_min, avg_dur_sec) requests_uploader = PlaylistSong.objects.filter(state=2).exclude( Q(user_id=None) @@ -139,7 +139,7 @@ def compute_stats(): most_requested_uploaders = [] best_uploaders_list(list(requests_uploader), most_requested_uploaders) - for time in list(time_requested): + for time in list(stats['time_requested']): # converts total time and average time in respectively days and minutes, seconds time['duration'] = str(round(time['total'] / 86400, 2)) + ' days' avg_dur_min, avg_dur_sec = divmod(time['avg_dur'], 60) @@ -148,23 +148,23 @@ def compute_stats(): time['avg_dur'] = '{}:{}'.format(avg_dur_min, avg_dur_sec) # Convert requested time to days - time_requested = list(time_requested) + time_requested = list(stats['time_requested']) for tr in time_requested: tr['duration'] = str(round(tr['total'] / 86400, 2)) + ' days' return { 'last_updated': last_updated, - 'total_uploads': total_uploads, - 'upload_stats': list(upload_stats), - 'total_requests': total_requests, - 'request_stats': list(request_stats), - 'unique_request_stats': list(unique_request_stats), - 'total_unique_requests': total_unique_requests, - 'most_played_songs': list(most_played_songs), - 'most_played_songs_14_days': list(most_played_songs_14_days), - 'time_requested': time_requested, + 'total_uploads': stats['total_uploads'], + 'upload_stats': list(stats['upload_stats']), + 'total_requests': stats['total_requests'], + 'request_stats': list(stats['request_stats']), + 'unique_request_stats': list(stats['unique_request_stats']), + 'total_unique_requests': stats['total_unique_requests'], + 'most_played_songs': list(stats['most_played_songs']), + 'most_played_songs_14_days': list(stats['most_played_songs_14_days']), + 'time_requested': stats['time_requested'], 'total_time_requested': str(round(float( - total_time_requested['total']) / 86400, 2)) + ' days', + stats['total_time_requested']['total']) / 86400, 2)) + ' days', 'stats_top_count': settings.STATS_TOP_COUNT, 'most_requested_uploaders': list(most_requested_uploaders), 'total_average': total_average,
' + artist + $('.queuebody:last-child').append('' + '' + artist + '' + title + '' + '