2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
*.pyc
|
*.pyc
|
||||||
|
*.log
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
*.bak
|
*.bak
|
||||||
.idea/
|
.idea/
|
||||||
@@ -9,3 +10,4 @@ api-keys.sh
|
|||||||
Pipfile
|
Pipfile
|
||||||
*.txt
|
*.txt
|
||||||
scrap.py
|
scrap.py
|
||||||
|
media/history/*
|
||||||
|
|||||||
10
api/management/commands/update-history.py
Normal file
10
api/management/commands/update-history.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from api.utils import parse_history
|
||||||
|
from login.models import User
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = 'Update history for users who requested it'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
user_id = "polarbier"
|
||||||
|
parse_history(User.objects.get(id=user_id).secret)
|
||||||
@@ -45,9 +45,8 @@ class Track(models.Model):
|
|||||||
verbose_name_plural = "Tracks"
|
verbose_name_plural = "Tracks"
|
||||||
|
|
||||||
id = models.CharField(primary_key=True, max_length=MAX_ID)
|
id = models.CharField(primary_key=True, max_length=MAX_ID)
|
||||||
# artist = models.ForeignKey(Artist, on_delete=models.CASCADE)
|
|
||||||
artists = models.ManyToManyField(Artist, blank=True)
|
artists = models.ManyToManyField(Artist, blank=True)
|
||||||
year = models.PositiveSmallIntegerField()
|
year = models.PositiveSmallIntegerField(null=True)
|
||||||
popularity = models.PositiveSmallIntegerField()
|
popularity = models.PositiveSmallIntegerField()
|
||||||
runtime = models.PositiveSmallIntegerField()
|
runtime = models.PositiveSmallIntegerField()
|
||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
@@ -86,3 +85,30 @@ class AudioFeatures(models.Model):
|
|||||||
return super(AudioFeatures, self).__str__()
|
return super(AudioFeatures, self).__str__()
|
||||||
|
|
||||||
# }}} AudioFeatures #
|
# }}} AudioFeatures #
|
||||||
|
|
||||||
|
# History {{{ #
|
||||||
|
|
||||||
|
class History(models.Model):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "History"
|
||||||
|
verbose_name_plural = "History"
|
||||||
|
unique_together = (("user", "timestamp"),)
|
||||||
|
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
timestamp = models.DateTimeField()
|
||||||
|
track = models.ForeignKey(Track, on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return " - ".join((str(self.user), str(self.timestamp), str(self.track)))
|
||||||
|
|
||||||
|
def get_artists(self):
|
||||||
|
artist_names = [artist.name for artist in self.track.artists.all()]
|
||||||
|
return ', '.join(artist_names)
|
||||||
|
|
||||||
|
def get_iso_timestamp(self):
|
||||||
|
return self.timestamp.isoformat()
|
||||||
|
|
||||||
|
# }}} #
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,16 @@ from .views import *
|
|||||||
|
|
||||||
app_name = 'api'
|
app_name = 'api'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('scan/<str:user_secret>', parse_library,
|
path('scan/library/<str:user_secret>', parse_library,
|
||||||
name='scan'),
|
name='scan_library'),
|
||||||
|
path('scan/history/<str:user_secret>', parse_history_request,
|
||||||
|
name='scan_history'),
|
||||||
path('user_artists/<str:user_secret>', get_artist_data,
|
path('user_artists/<str:user_secret>', get_artist_data,
|
||||||
name='get_artist_data'),
|
name='get_artist_data'),
|
||||||
path('user_genres/<str:user_secret>', get_genre_data,
|
path('user_genres/<str:user_secret>', get_genre_data,
|
||||||
name='get_genre_data'),
|
name='get_genre_data'),
|
||||||
path('audio_features/<str:audio_feature>/<str:user_secret>',
|
path('audio_features/<str:audio_feature>/<str:user_secret>',
|
||||||
get_audio_feature_data, name='get_audio_feature_data'),
|
get_audio_feature_data, name='get_audio_feature_data'),
|
||||||
|
path('import/history/<upload_id>', import_history, name='import_history'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
178
api/utils.py
178
api/utils.py
@@ -1,21 +1,26 @@
|
|||||||
# imports {{{ #
|
# imports {{{ #
|
||||||
import requests
|
import requests
|
||||||
import math
|
import math
|
||||||
import pprint
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from django.db.models import Count, Q, F
|
from django.db.models import Count, F, Max
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.core import serializers
|
from django.core import serializers
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from .models import *
|
from .models import *
|
||||||
|
from . import views
|
||||||
from login.models import User
|
from login.models import User
|
||||||
|
from pprint import pprint
|
||||||
|
from dateutil.parser import parse
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
HISTORY_ENDPOINT = 'https://api.spotify.com/v1/me/player/recently-played'
|
||||||
|
|
||||||
# }}} imports #
|
# }}} imports #
|
||||||
|
|
||||||
console_logging = True
|
# console_logging = True
|
||||||
# console_logging = False
|
console_logging = False
|
||||||
artists_genre_processed = 0
|
artists_genre_processed = 0
|
||||||
features_processed = 0
|
features_processed = 0
|
||||||
|
|
||||||
@@ -74,19 +79,32 @@ def save_track_obj(track_dict, artists, user_obj):
|
|||||||
if len(track_query) != 0:
|
if len(track_query) != 0:
|
||||||
return track_query[0], False
|
return track_query[0], False
|
||||||
else:
|
else:
|
||||||
new_track = Track.objects.create(
|
# check if track is simple or full, simple Track object won't have year
|
||||||
id=track_dict['id'],
|
# if 'album' in track_dict:
|
||||||
year=track_dict['album']['release_date'].split('-')[0],
|
try:
|
||||||
popularity=int(track_dict['popularity']),
|
new_track = Track.objects.create(
|
||||||
runtime=int(float(track_dict['duration_ms']) / 1000),
|
id=track_dict['id'],
|
||||||
name=track_dict['name'],
|
year=track_dict['album']['release_date'].split('-')[0],
|
||||||
)
|
popularity=int(track_dict['popularity']),
|
||||||
|
runtime=int(float(track_dict['duration_ms']) / 1000),
|
||||||
|
name=track_dict['name'],
|
||||||
|
)
|
||||||
|
# else:
|
||||||
|
except KeyError:
|
||||||
|
new_track = Track.objects.create(
|
||||||
|
id=track_dict['id'],
|
||||||
|
popularity=int(track_dict['popularity']),
|
||||||
|
runtime=int(float(track_dict['duration_ms']) / 1000),
|
||||||
|
name=track_dict['name'],
|
||||||
|
)
|
||||||
|
|
||||||
# have to add artists and user_obj after saving object since track needs to
|
# have to add artists and user_obj after saving object since track needs to
|
||||||
# have ID before filling in m2m field
|
# have ID before filling in m2m field
|
||||||
for artist in artists:
|
for artist in artists:
|
||||||
new_track.artists.add(artist)
|
new_track.artists.add(artist)
|
||||||
new_track.users.add(user_obj)
|
# print(new_track.name, artist.name)
|
||||||
|
if user_obj != None:
|
||||||
|
new_track.users.add(user_obj)
|
||||||
new_track.save()
|
new_track.save()
|
||||||
return new_track, True
|
return new_track, True
|
||||||
|
|
||||||
@@ -169,8 +187,8 @@ def add_artist_genres(headers, artist_objs):
|
|||||||
artist_ids = str.join(",", [artist_obj.id for artist_obj in artist_objs])
|
artist_ids = str.join(",", [artist_obj.id for artist_obj in artist_objs])
|
||||||
params = {'ids': artist_ids}
|
params = {'ids': artist_ids}
|
||||||
artists_response = requests.get('https://api.spotify.com/v1/artists/',
|
artists_response = requests.get('https://api.spotify.com/v1/artists/',
|
||||||
headers=headers,
|
headers=headers,
|
||||||
params=params,
|
params={'ids': artist_ids},
|
||||||
).json()['artists']
|
).json()['artists']
|
||||||
for i in range(len(artist_objs)):
|
for i in range(len(artist_objs)):
|
||||||
if len(artists_response[i]['genres']) == 0:
|
if len(artists_response[i]['genres']) == 0:
|
||||||
@@ -178,6 +196,7 @@ def add_artist_genres(headers, artist_objs):
|
|||||||
else:
|
else:
|
||||||
for genre in artists_response[i]['genres']:
|
for genre in artists_response[i]['genres']:
|
||||||
process_artist_genre(genre, artist_objs[i])
|
process_artist_genre(genre, artist_objs[i])
|
||||||
|
# print(artist_objs[i].name, genre)
|
||||||
|
|
||||||
if console_logging:
|
if console_logging:
|
||||||
global artists_genre_processed
|
global artists_genre_processed
|
||||||
@@ -221,6 +240,35 @@ def get_artists_in_genre(user, genre, max_songs):
|
|||||||
|
|
||||||
# }}} get_artists_in_genre #
|
# }}} get_artists_in_genre #
|
||||||
|
|
||||||
|
# save_track_artists {{{ #
|
||||||
|
|
||||||
|
def save_track_artists(track_dict, artist_genre_queue, user_headers):
|
||||||
|
""" Update artist info before creating Track so that Track object can
|
||||||
|
reference Artist object.
|
||||||
|
|
||||||
|
:track_dict: response from Spotify API for track
|
||||||
|
:returns: list of Artist objects in Track
|
||||||
|
|
||||||
|
"""
|
||||||
|
track_artists = []
|
||||||
|
for artist_dict in track_dict['artists']:
|
||||||
|
artist_obj, artist_created = Artist.objects.get_or_create(
|
||||||
|
id=artist_dict['id'],
|
||||||
|
name=artist_dict['name'],)
|
||||||
|
# only add/tally up artist genres if new
|
||||||
|
if artist_created:
|
||||||
|
artist_genre_queue.append(artist_obj)
|
||||||
|
if len(artist_genre_queue) == views.ARTIST_LIMIT:
|
||||||
|
add_artist_genres(user_headers, artist_genre_queue)
|
||||||
|
artist_genre_queue[:] = []
|
||||||
|
track_artists.append(artist_obj)
|
||||||
|
|
||||||
|
return track_artists
|
||||||
|
|
||||||
|
# }}} save_track_artists #
|
||||||
|
|
||||||
|
# get_user_header {{{ #
|
||||||
|
|
||||||
def get_user_header(user_obj):
|
def get_user_header(user_obj):
|
||||||
"""Returns the authorization string needed to make an API call.
|
"""Returns the authorization string needed to make an API call.
|
||||||
|
|
||||||
@@ -246,3 +294,105 @@ def get_user_header(user_obj):
|
|||||||
user_obj.save()
|
user_obj.save()
|
||||||
|
|
||||||
return {'Authorization': "Bearer " + user_obj.access_token}
|
return {'Authorization': "Bearer " + user_obj.access_token}
|
||||||
|
|
||||||
|
# }}} get_user_header #
|
||||||
|
|
||||||
|
# save_history_obj {{{ #
|
||||||
|
|
||||||
|
def save_history_obj (user, timestamp, track):
|
||||||
|
"""Return (get/create) a History object with the specified parameters. Can't
|
||||||
|
use built-in get_or_create since don't know auto PK.
|
||||||
|
|
||||||
|
:user: User object History should be associated with
|
||||||
|
:timestamp: time at which song was listened to
|
||||||
|
:track: Track object for song
|
||||||
|
:returns: History object
|
||||||
|
|
||||||
|
"""
|
||||||
|
history_query = History.objects.filter(user__exact=user,
|
||||||
|
timestamp__exact=timestamp)
|
||||||
|
if len(history_query) == 0:
|
||||||
|
history_obj = History.objects.create(user=user, timestamp=timestamp,
|
||||||
|
track=track)
|
||||||
|
else:
|
||||||
|
history_obj = history_query[0]
|
||||||
|
|
||||||
|
return history_obj
|
||||||
|
|
||||||
|
# }}} save_history_obj #
|
||||||
|
|
||||||
|
# get_next_history_row {{{ #
|
||||||
|
|
||||||
|
def get_next_history_row(csv_reader, headers, prev_info):
|
||||||
|
"""Return formatted information from next row in history CSV file.
|
||||||
|
|
||||||
|
:csv_reader: TODO
|
||||||
|
:headers:
|
||||||
|
:prev_info: history_obj_info of last row in case no more rows
|
||||||
|
:returns: (boolean of if last row, dict with information of next row)
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
row = next(csv_reader)
|
||||||
|
# if Track.objects.filter(id__exact=row[1]).exists():
|
||||||
|
history_obj_info = {}
|
||||||
|
for i in range(len(headers)):
|
||||||
|
history_obj_info[headers[i]] = row[i]
|
||||||
|
return False, history_obj_info
|
||||||
|
except StopIteration:
|
||||||
|
return True, prev_info
|
||||||
|
|
||||||
|
# }}} get_next_history_row #
|
||||||
|
|
||||||
|
# parse_history {{{ #
|
||||||
|
|
||||||
|
def parse_history(user_secret):
|
||||||
|
"""Scans user's listening history and stores the information in a
|
||||||
|
database.
|
||||||
|
|
||||||
|
:user_secret: secret for User object who's library is being scanned.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
user_obj = User.objects.get(secret=user_secret)
|
||||||
|
payload = {'limit': str(views.USER_TRACKS_LIMIT)}
|
||||||
|
last_time_played = History.objects.filter(user=user_obj).aggregate(Max('timestamp'))['timestamp__max']
|
||||||
|
if last_time_played is not None:
|
||||||
|
payload['after'] = last_time_played.isoformat()
|
||||||
|
artist_genre_queue = []
|
||||||
|
user_headers = get_user_header(user_obj)
|
||||||
|
history_response = requests.get(HISTORY_ENDPOINT,
|
||||||
|
headers=user_headers,
|
||||||
|
params=payload).json()['items']
|
||||||
|
# pprint(history_response)
|
||||||
|
|
||||||
|
tracks_processed = 0
|
||||||
|
|
||||||
|
for track_dict in history_response:
|
||||||
|
# don't associate history track with User, not necessarily in their
|
||||||
|
# library
|
||||||
|
# track_obj, track_created = save_track_obj(track_dict['track'],
|
||||||
|
# track_artists, None)
|
||||||
|
track_artists = save_track_artists(track_dict['track'], artist_genre_queue,
|
||||||
|
user_headers)
|
||||||
|
track_obj, track_created = save_track_obj(track_dict['track'],
|
||||||
|
track_artists, None)
|
||||||
|
history_obj = save_history_obj(user_obj, parse(track_dict['played_at']),
|
||||||
|
track_obj)
|
||||||
|
tracks_processed += 1
|
||||||
|
|
||||||
|
if console_logging:
|
||||||
|
print("Added history track #{}: {}".format(
|
||||||
|
tracks_processed, history_obj,))
|
||||||
|
|
||||||
|
if len(artist_genre_queue) > 0:
|
||||||
|
add_artist_genres(user_headers, artist_genre_queue)
|
||||||
|
|
||||||
|
# TODO: update track genres from History relation
|
||||||
|
# update_track_genres(user_obj)
|
||||||
|
|
||||||
|
print("Scanned {} history tracks for user {} at {}.".format(
|
||||||
|
tracks_processed, user_obj.id, datetime.now()))
|
||||||
|
|
||||||
|
# }}} get_history #
|
||||||
|
|
||||||
|
|||||||
139
api/views.py
139
api/views.py
@@ -5,27 +5,39 @@ import random
|
|||||||
import requests
|
import requests
|
||||||
import urllib
|
import urllib
|
||||||
import secrets
|
import secrets
|
||||||
import pprint
|
|
||||||
import string
|
import string
|
||||||
|
import csv
|
||||||
|
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db.models import Count, Q
|
from django.db.models import Count, Q, Max
|
||||||
|
from django.core.files import File
|
||||||
from .utils import *
|
from .utils import *
|
||||||
from .models import *
|
from .models import *
|
||||||
from login.models import User
|
from login.models import User
|
||||||
from login.utils import get_user_context
|
from login.utils import get_user_context
|
||||||
|
from dateutil.parser import parse
|
||||||
|
from pprint import pprint
|
||||||
|
from login.models import HistoryUpload
|
||||||
|
|
||||||
# }}} imports #
|
# }}} imports #
|
||||||
|
|
||||||
|
# constants {{{ #
|
||||||
|
|
||||||
USER_TRACKS_LIMIT = 50
|
USER_TRACKS_LIMIT = 50
|
||||||
|
TRACKS_LIMIT = 50
|
||||||
|
HISTORY_LIMIT = 50
|
||||||
ARTIST_LIMIT = 50
|
ARTIST_LIMIT = 50
|
||||||
FEATURES_LIMIT = 100
|
FEATURES_LIMIT = 100
|
||||||
# ARTIST_LIMIT = 25
|
# ARTIST_LIMIT = 25
|
||||||
# FEATURES_LIMIT = 25
|
# FEATURES_LIMIT = 25
|
||||||
TRACKS_TO_QUERY = 100
|
TRACKS_TO_QUERY = 100
|
||||||
|
TRACKS_ENDPOINT = 'https://api.spotify.com/v1/tracks'
|
||||||
|
|
||||||
console_logging = True
|
console_logging = True
|
||||||
|
# console_logging = False
|
||||||
|
|
||||||
|
# }}} constants #
|
||||||
|
|
||||||
# parse_library {{{ #
|
# parse_library {{{ #
|
||||||
|
|
||||||
@@ -47,7 +59,8 @@ def parse_library(request, user_secret):
|
|||||||
# create this obj so loop runs at least once
|
# create this obj so loop runs at least once
|
||||||
saved_tracks_response = [0]
|
saved_tracks_response = [0]
|
||||||
# scan until reach num_tracks or no tracks left if scanning entire library
|
# scan until reach num_tracks or no tracks left if scanning entire library
|
||||||
while (TRACKS_TO_QUERY == 0 or offset < TRACKS_TO_QUERY) and len(saved_tracks_response) > 0:
|
while ((TRACKS_TO_QUERY == 0 or offset < TRACKS_TO_QUERY) and
|
||||||
|
len(saved_tracks_response) > 0):
|
||||||
payload['offset'] = str(offset)
|
payload['offset'] = str(offset)
|
||||||
saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
|
saved_tracks_response = requests.get('https://api.spotify.com/v1/me/tracks',
|
||||||
headers=user_headers,
|
headers=user_headers,
|
||||||
@@ -57,25 +70,8 @@ def parse_library(request, user_secret):
|
|||||||
tracks_processed = 0
|
tracks_processed = 0
|
||||||
|
|
||||||
for track_dict in saved_tracks_response:
|
for track_dict in saved_tracks_response:
|
||||||
# add artists {{{ #
|
track_artists = save_track_artists(track_dict['track'], artist_genre_queue,
|
||||||
|
user_headers)
|
||||||
# update artist info before track so that Track object can reference
|
|
||||||
# Artist object
|
|
||||||
track_artists = []
|
|
||||||
for artist_dict in track_dict['track']['artists']:
|
|
||||||
artist_obj, artist_created = Artist.objects.get_or_create(
|
|
||||||
id=artist_dict['id'],
|
|
||||||
name=artist_dict['name'],)
|
|
||||||
# only add/tally up artist genres if new
|
|
||||||
if artist_created:
|
|
||||||
artist_genre_queue.append(artist_obj)
|
|
||||||
if len(artist_genre_queue) == ARTIST_LIMIT:
|
|
||||||
add_artist_genres(user_headers, artist_genre_queue)
|
|
||||||
artist_genre_queue = []
|
|
||||||
track_artists.append(artist_obj)
|
|
||||||
|
|
||||||
# }}} add artists #
|
|
||||||
|
|
||||||
track_obj, track_created = save_track_obj(track_dict['track'],
|
track_obj, track_created = save_track_obj(track_dict['track'],
|
||||||
track_artists, user_obj)
|
track_artists, user_obj)
|
||||||
|
|
||||||
@@ -119,6 +115,21 @@ def parse_library(request, user_secret):
|
|||||||
|
|
||||||
# }}} parse_library #
|
# }}} parse_library #
|
||||||
|
|
||||||
|
# parse_history_request {{{ #
|
||||||
|
|
||||||
|
def parse_history_request(request, user_secret):
|
||||||
|
"""Request function to call parse_history. Scans user's listening history
|
||||||
|
and stores the information in a database.
|
||||||
|
|
||||||
|
:user_secret: secret for User object who's library is being scanned.
|
||||||
|
:returns: redirects user to logged in page
|
||||||
|
"""
|
||||||
|
parse_history(user_secret)
|
||||||
|
return render(request, 'graphs/logged_in.html',
|
||||||
|
get_user_context(User.objects.get(secret=user_secret)))
|
||||||
|
|
||||||
|
# }}} get_history #
|
||||||
|
|
||||||
# get_artist_data {{{ #
|
# get_artist_data {{{ #
|
||||||
|
|
||||||
def get_artist_data(request, user_secret):
|
def get_artist_data(request, user_secret):
|
||||||
@@ -134,7 +145,7 @@ def get_artist_data(request, user_secret):
|
|||||||
filter=Q(track__users=user)))
|
filter=Q(track__users=user)))
|
||||||
processed_artist_counts = [{'name': artist.name, 'num_songs': artist.num_songs}
|
processed_artist_counts = [{'name': artist.name, 'num_songs': artist.num_songs}
|
||||||
for artist in artist_counts]
|
for artist in artist_counts]
|
||||||
pprint.pprint(processed_artist_counts)
|
pprint(processed_artist_counts)
|
||||||
return JsonResponse(data=processed_artist_counts, safe=False)
|
return JsonResponse(data=processed_artist_counts, safe=False)
|
||||||
|
|
||||||
# }}} get_artist_data #
|
# }}} get_artist_data #
|
||||||
@@ -181,7 +192,87 @@ def get_genre_data(request, user_secret):
|
|||||||
genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'],
|
genre_dict['artists'] = get_artists_in_genre(user, genre_dict['genre'],
|
||||||
genre_dict['num_songs'])
|
genre_dict['num_songs'])
|
||||||
print("*** Genre Breakdown ***")
|
print("*** Genre Breakdown ***")
|
||||||
pprint.pprint(list(genre_counts))
|
pprint(list(genre_counts))
|
||||||
return JsonResponse(data=list(genre_counts), safe=False)
|
return JsonResponse(data=list(genre_counts), safe=False)
|
||||||
|
|
||||||
# }}} get_genre_data #
|
# }}} get_genre_data #
|
||||||
|
|
||||||
|
# import_history {{{ #
|
||||||
|
|
||||||
|
def import_history(request, upload_id):
|
||||||
|
"""Import history for the user from the file they uploaded.
|
||||||
|
|
||||||
|
:upload_id: ID (PK) of the HistoryUpload entry
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
# setup {{{ #
|
||||||
|
|
||||||
|
headers = ['timestamp', 'track_id']
|
||||||
|
upload_obj = HistoryUpload.objects.get(id=upload_id)
|
||||||
|
user_headers = get_user_header(upload_obj.user)
|
||||||
|
|
||||||
|
with upload_obj.document.open('r') as f:
|
||||||
|
csv_reader = csv.reader(f, delimiter=',')
|
||||||
|
rows_read = 0
|
||||||
|
history_obj_info_lst = []
|
||||||
|
artist_genre_queue = []
|
||||||
|
|
||||||
|
# skip header row
|
||||||
|
last_row, history_obj_info = get_next_history_row(csv_reader, headers,
|
||||||
|
{})
|
||||||
|
while not last_row:
|
||||||
|
last_row, history_obj_info = get_next_history_row(csv_reader,
|
||||||
|
headers, history_obj_info)
|
||||||
|
|
||||||
|
# }}} setup #
|
||||||
|
|
||||||
|
history_obj_info_lst.append(history_obj_info)
|
||||||
|
# PU: refactor saving History object right away if Track obj already
|
||||||
|
# exists
|
||||||
|
# PU: refactor below?
|
||||||
|
rows_read += 1
|
||||||
|
if (rows_read % TRACKS_LIMIT == 0) or last_row:
|
||||||
|
# get tracks_response {{{ #
|
||||||
|
|
||||||
|
track_ids_lst = [info['track_id'] for info in history_obj_info_lst]
|
||||||
|
# print(len(track_ids_lst))
|
||||||
|
track_ids = ','.join(track_ids_lst)
|
||||||
|
payload = {'ids': track_ids}
|
||||||
|
tracks_response = requests.get(TRACKS_ENDPOINT,
|
||||||
|
headers=user_headers,
|
||||||
|
params=payload).json()['tracks']
|
||||||
|
responses_processed = 0
|
||||||
|
|
||||||
|
# }}} get tracks_response #
|
||||||
|
|
||||||
|
for track_dict in tracks_response:
|
||||||
|
# don't associate history track with User, not necessarily in their
|
||||||
|
# library
|
||||||
|
track_artists = save_track_artists(track_dict, artist_genre_queue,
|
||||||
|
user_headers)
|
||||||
|
track_obj, track_created = save_track_obj(track_dict,
|
||||||
|
track_artists, None)
|
||||||
|
|
||||||
|
timestamp = \
|
||||||
|
parse(history_obj_info_lst[responses_processed]['timestamp'])
|
||||||
|
history_obj = save_history_obj(upload_obj.user, timestamp,
|
||||||
|
track_obj)
|
||||||
|
|
||||||
|
if console_logging:
|
||||||
|
print("Processed row #{}: {}".format(
|
||||||
|
(rows_read - TRACKS_LIMIT) + responses_processed, history_obj,))
|
||||||
|
responses_processed += 1
|
||||||
|
|
||||||
|
history_obj_info_lst = []
|
||||||
|
|
||||||
|
if len(artist_genre_queue) > 0:
|
||||||
|
add_artist_genres(user_headers, artist_genre_queue)
|
||||||
|
|
||||||
|
# TODO: update track genres from History relation
|
||||||
|
# update_track_genres(user_obj)
|
||||||
|
|
||||||
|
return redirect('graphs:display_history_table')
|
||||||
|
|
||||||
|
# }}} get_history #
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,18 @@
|
|||||||
<body>
|
<body>
|
||||||
<script src="https://d3js.org/d3.v5.min.js"></script>
|
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/randomcolor/0.5.2/randomColor.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/randomcolor/0.5.2/randomColor.min.js"></script>
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<script src="{% static "graphs/scripts/genre_graph.js" %}"></script>
|
<script src="{% static "graphs/scripts/genre_graph.js" %}"></script>
|
||||||
|
|
||||||
<svg width="1920" height="740"></svg>
|
<!-- <div class="row">
|
||||||
|
<div class="col-" id="genre-graph"></div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<svg id="genre-graph" width="600" height="400"
|
||||||
|
viewBox="0 0 600 400"
|
||||||
|
perserveAspectRatio="xMinYMid">
|
||||||
|
</svg>
|
||||||
<script>
|
<script>
|
||||||
var svg = d3.select("svg"),
|
var svg = d3.select("svg"),
|
||||||
margin = {top: 20, right: 20, bottom: 30, left: 40},
|
margin = {top: 20, right: 20, bottom: 30, left: 40},
|
||||||
@@ -38,7 +46,7 @@
|
|||||||
var y = d3.scaleLinear()
|
var y = d3.scaleLinear()
|
||||||
.rangeRound([height, 0]);
|
.rangeRound([height, 0]);
|
||||||
|
|
||||||
d3.json("{% url "api:get_genre_data" user_secret %}").then(create_genre_graph);
|
d3.json("{% url "api:get_genre_data" user_secret %}").then(create_genre_graph);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -16,5 +16,8 @@
|
|||||||
<a class="btn btn-primary" href="{% url "graphs:display_artist_graph" user_secret %}" role="button">
|
<a class="btn btn-primary" href="{% url "graphs:display_artist_graph" user_secret %}" role="button">
|
||||||
Artists
|
Artists
|
||||||
</a>
|
</a>
|
||||||
|
<a class="btn btn-primary" href="{% url "graphs:display_history_table" %}" role="button">
|
||||||
|
History
|
||||||
|
</a>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
17
graphs/templates/graphs/user_history.html
Normal file
17
graphs/templates/graphs/user_history.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
{% load static %}
|
||||||
|
{% load render_table export_url from django_tables2 %}
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>User History</title>
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
|
||||||
|
<link rel="stylesheet" href="{% static 'css/dark_bg.css' %}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>{{ user_id }}'s Listening History</h1>
|
||||||
|
<p> Found {{ total_history }} songs. </p>
|
||||||
|
<a class="btn btn-primary " href="{% export_url 'csv' %}" role="button">Export</a>
|
||||||
|
{% render_table user_history_table %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -10,4 +10,5 @@ urlpatterns = [
|
|||||||
name='display_genre_graph'),
|
name='display_genre_graph'),
|
||||||
path('audio_features/<str:user_secret>', display_features_graphs,
|
path('audio_features/<str:user_secret>', display_features_graphs,
|
||||||
name='display_audio_features'),
|
name='display_audio_features'),
|
||||||
|
path('history/', HistoryList.as_view(), name='display_history_table'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
import django_tables2 as tables
|
||||||
|
|
||||||
|
from pprint import pprint
|
||||||
|
from login.models import User
|
||||||
|
from api.models import History
|
||||||
|
|
||||||
|
class HistoryTable(tables.Table):
|
||||||
|
class Meta:
|
||||||
|
model = History
|
||||||
|
template_name = 'django_tables2/bootstrap.html'
|
||||||
|
|
||||||
|
iso_timestamp = tables.Column(accessor='get_iso_timestamp', orderable=False)
|
||||||
|
track_id = tables.Column(accessor='track.id', orderable=False)
|
||||||
|
track_name = tables.Column(accessor='track.name', orderable=False)
|
||||||
|
artists = tables.Column(accessor='get_artists', orderable=False)
|
||||||
|
|
||||||
def get_secret_context(user_secret):
|
def get_secret_context(user_secret):
|
||||||
"""Return user_secret in context for graph pages.
|
"""Return user_secret in context for graph pages.
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,17 @@ import requests
|
|||||||
import os
|
import os
|
||||||
import urllib
|
import urllib
|
||||||
import secrets
|
import secrets
|
||||||
import pprint
|
|
||||||
import string
|
import string
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
|
from pprint import pprint
|
||||||
|
from datetime import datetime
|
||||||
|
from time import strftime
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from .utils import *
|
from .utils import *
|
||||||
|
from django_tables2 import RequestConfig, SingleTableView
|
||||||
|
from django_tables2.export.views import ExportMixin
|
||||||
|
from django_tables2.export import TableExport
|
||||||
|
from api.models import History
|
||||||
|
|
||||||
# }}} imports #
|
# }}} imports #
|
||||||
|
|
||||||
@@ -40,3 +45,48 @@ def display_features_graphs(request, user_secret):
|
|||||||
"""
|
"""
|
||||||
return render(request, "graphs/features_graphs.html",
|
return render(request, "graphs/features_graphs.html",
|
||||||
get_secret_context(user_secret))
|
get_secret_context(user_secret))
|
||||||
|
|
||||||
|
# HistoryList {{{ #
|
||||||
|
|
||||||
|
class HistoryList(ExportMixin, SingleTableView):
|
||||||
|
"""Create table with list of song history."""
|
||||||
|
model = History
|
||||||
|
table_class = HistoryTable
|
||||||
|
context_table_name = 'user_history_table'
|
||||||
|
template_name = 'graphs/user_history.html'
|
||||||
|
|
||||||
|
# overridden methods {{{ #
|
||||||
|
|
||||||
|
def get_table_kwargs(self):
|
||||||
|
return { 'exclude': ('id', 'user', 'track', 'track_id', 'iso_timestamp', ) }
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
return History.objects.filter(user__exact=self.request.session['user_id']).order_by('-timestamp')
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
context['user_id'] = self.request.session['user_id']
|
||||||
|
context['total_history'] = self.get_table_data().count()
|
||||||
|
return context
|
||||||
|
|
||||||
|
def get_export_filename(self, export_format):
|
||||||
|
user_id = self.request.session['user_id']
|
||||||
|
# timestamp = strftime("%m%d%Y-%H%M")
|
||||||
|
# return "{}.{}".format("-".join((user_id, timestamp)), export_format)
|
||||||
|
return "{}.{}".format(user_id, export_format)
|
||||||
|
|
||||||
|
def create_export(self, export_format):
|
||||||
|
export_exclude = ('id', 'user', 'track', 'track_name', 'artists',
|
||||||
|
'timestamp', )
|
||||||
|
exporter = TableExport(
|
||||||
|
export_format=export_format,
|
||||||
|
table=self.get_table(exclude=export_exclude),
|
||||||
|
exclude_columns=self.exclude_columns,
|
||||||
|
)
|
||||||
|
|
||||||
|
return exporter.response(filename=self.get_export_filename(export_format))
|
||||||
|
|
||||||
|
# }}} overridden methods #
|
||||||
|
|
||||||
|
# }}} HistoryList #
|
||||||
|
|
||||||
|
|||||||
8
login/forms.py
Normal file
8
login/forms.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
from django import forms
|
||||||
|
from .models import HistoryUpload
|
||||||
|
|
||||||
|
class HistoryUploadForm(forms.ModelForm):
|
||||||
|
class Meta:
|
||||||
|
model = HistoryUpload
|
||||||
|
fields = ('user', 'document', )
|
||||||
|
widgets = { 'user': forms.HiddenInput() }
|
||||||
@@ -20,3 +20,8 @@ class User(models.Model):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.id
|
return self.id
|
||||||
|
|
||||||
|
class HistoryUpload(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
document = models.FileField(upload_to='history/')
|
||||||
|
uploaded_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div id="login">
|
<div id="login">
|
||||||
<h1>spotify-lib-vis</h1>
|
<h1>spotify-lib-vis</h1>
|
||||||
<a href="{% url 'login:spotify_login' %}" class="btn btn-primary">Login</a>
|
<a href="{% url 'login:spotify_login' %}" class="btn btn-primary" role="button">Login</a>
|
||||||
<a href="{% url 'login:admin_graphs' %}" class="btn btn-primary">Admin Graphs</a>
|
<a href="{% url 'login:admin_graphs' %}" class="btn btn-primary">Admin Graphs</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,8 +18,16 @@
|
|||||||
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
|
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="#">upgrade your browser</a> to improve your experience.</p>
|
||||||
<![endif]-->
|
<![endif]-->
|
||||||
<h1>Logged in as {{ user_id }}</h1>
|
<h1>Logged in as {{ user_id }}</h1>
|
||||||
<a href="{% url "api:scan" user_secret %}" class="btn btn-primary">
|
<a href="{% url "api:scan_library" user_secret %}" class="btn btn-primary">
|
||||||
Scan Library
|
Scan Library
|
||||||
</a>
|
</a>
|
||||||
|
<a href="{% url "api:scan_history" user_secret %}" class="btn btn-primary">
|
||||||
|
Scan History
|
||||||
|
</a>
|
||||||
|
<form action="{% url 'login:upload_history' %}" method="post" enctype="multipart/form-data">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<input class="btn btn-primary" type="submit" value="Import History">
|
||||||
|
</form>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ urlpatterns = [
|
|||||||
path('callback', callback, name='callback'),
|
path('callback', callback, name='callback'),
|
||||||
# path('user/<str:user_secret>', user_home, name='user_home'),
|
# path('user/<str:user_secret>', user_home, name='user_home'),
|
||||||
path('admin_graphs', admin_graphs, name='admin_graphs'),
|
path('admin_graphs', admin_graphs, name='admin_graphs'),
|
||||||
|
path('upload_history', upload_history, name='upload_history'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
|
import string
|
||||||
|
import random
|
||||||
|
import requests
|
||||||
|
import secrets
|
||||||
|
|
||||||
from .models import User
|
from .models import User
|
||||||
|
from .forms import HistoryUploadForm
|
||||||
|
|
||||||
def get_user_context(user_obj):
|
def get_user_context(user_obj):
|
||||||
"""Get context for rendering with User's ID and secret.
|
"""Get context for rendering with User's ID and secret.
|
||||||
@@ -8,3 +14,70 @@ def get_user_context(user_obj):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
return { 'user_id': user_obj.id, 'user_secret': user_obj.secret, }
|
return { 'user_id': user_obj.id, 'user_secret': user_obj.secret, }
|
||||||
|
|
||||||
|
# generate_random_string {{{ #
|
||||||
|
|
||||||
|
def generate_random_string(length):
|
||||||
|
"""Generates a random string of a certain length
|
||||||
|
|
||||||
|
Args:
|
||||||
|
length: the desired length of the randomized string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A random string
|
||||||
|
"""
|
||||||
|
all_chars = string.ascii_letters + string.digits
|
||||||
|
rand_str = "".join(random.choice(all_chars) for _ in range(length))
|
||||||
|
|
||||||
|
return rand_str
|
||||||
|
|
||||||
|
# }}} generate_random_string #
|
||||||
|
|
||||||
|
# create_user {{{ #
|
||||||
|
|
||||||
|
|
||||||
|
def create_user(refresh_token, access_token, access_expires_in):
|
||||||
|
"""Create a User object based on information returned from Step 2 (callback
|
||||||
|
function) of auth flow.
|
||||||
|
|
||||||
|
:refresh_token: Used to renew access tokens.
|
||||||
|
:access_token: Used in Spotify API calls.
|
||||||
|
:access_expires_in: How long the access token last in seconds.
|
||||||
|
|
||||||
|
:returns: The newly created User object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
profile_response = requests.get('https://api.spotify.com/v1/me',
|
||||||
|
headers={'Authorization': "Bearer " + access_token}).json()
|
||||||
|
user_id = profile_response['id']
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_obj = User.objects.get(id=user_id)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
# Python docs recommends 32 bytes of randomness against brute
|
||||||
|
# force attacks
|
||||||
|
user_obj = User.objects.create(
|
||||||
|
id=user_id,
|
||||||
|
secret=secrets.token_urlsafe(32),
|
||||||
|
refresh_token=refresh_token,
|
||||||
|
access_token=access_token,
|
||||||
|
access_expires_in=access_expires_in,
|
||||||
|
)
|
||||||
|
|
||||||
|
return user_obj
|
||||||
|
|
||||||
|
# }}} create_user #
|
||||||
|
|
||||||
|
def get_scan_context(request):
|
||||||
|
"""Get context for rendering scan page.
|
||||||
|
|
||||||
|
:request:
|
||||||
|
:returns: Context with upload form and user info.
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = { 'user_id': request.session['user_id'],
|
||||||
|
'user_secret': request.session['user_secret'], }
|
||||||
|
# set hidden user field to current user
|
||||||
|
context['form'] = HistoryUploadForm(initial=
|
||||||
|
{ 'user': User.objects.get(id=request.session['user_id']) })
|
||||||
|
return context
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
# imports {{{ #
|
# imports {{{ #
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import random
|
|
||||||
import requests
|
|
||||||
import os
|
import os
|
||||||
import urllib
|
import urllib
|
||||||
import secrets
|
|
||||||
import pprint
|
import pprint
|
||||||
import string
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
@@ -19,29 +15,10 @@ from .utils import *
|
|||||||
|
|
||||||
TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
|
TIME_FORMAT = '%Y-%m-%d-%H-%M-%S'
|
||||||
TRACKS_TO_QUERY = 200
|
TRACKS_TO_QUERY = 200
|
||||||
|
AUTH_SCOPE = ['user-library-read', 'user-read-recently-played',]
|
||||||
# generate_random_string {{{ #
|
|
||||||
|
|
||||||
|
|
||||||
def generate_random_string(length):
|
|
||||||
"""Generates a random string of a certain length
|
|
||||||
|
|
||||||
Args:
|
|
||||||
length: the desired length of the randomized string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A random string
|
|
||||||
"""
|
|
||||||
all_chars = string.ascii_letters + string.digits
|
|
||||||
rand_str = "".join(random.choice(all_chars) for _ in range(length))
|
|
||||||
|
|
||||||
return rand_str
|
|
||||||
|
|
||||||
# }}} generate_random_string #
|
|
||||||
|
|
||||||
# index {{{ #
|
# index {{{ #
|
||||||
|
|
||||||
# Create your views here.
|
|
||||||
def index(request):
|
def index(request):
|
||||||
return render(request, 'login/index.html')
|
return render(request, 'login/index.html')
|
||||||
|
|
||||||
@@ -62,7 +39,7 @@ def spotify_login(request):
|
|||||||
'response_type': 'code',
|
'response_type': 'code',
|
||||||
'redirect_uri': 'http://localhost:8000/login/callback',
|
'redirect_uri': 'http://localhost:8000/login/callback',
|
||||||
'state': state_str,
|
'state': state_str,
|
||||||
'scope': 'user-library-read',
|
'scope': " ".join(AUTH_SCOPE),
|
||||||
'show_dialog': False
|
'show_dialog': False
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +49,8 @@ def spotify_login(request):
|
|||||||
|
|
||||||
# }}} spotify_login #
|
# }}} spotify_login #
|
||||||
|
|
||||||
|
# callback {{{ #
|
||||||
|
|
||||||
def callback(request):
|
def callback(request):
|
||||||
""" Step 2 in authorization flow: Have your application request refresh and
|
""" Step 2 in authorization flow: Have your application request refresh and
|
||||||
access tokens; Spotify returns access and refresh tokens.
|
access tokens; Spotify returns access and refresh tokens.
|
||||||
@@ -94,40 +73,13 @@ def callback(request):
|
|||||||
user_obj = create_user(token_response['refresh_token'],
|
user_obj = create_user(token_response['refresh_token'],
|
||||||
token_response['access_token'],
|
token_response['access_token'],
|
||||||
token_response['expires_in'])
|
token_response['expires_in'])
|
||||||
|
|
||||||
|
request.session['user_id'] = user_obj.id
|
||||||
|
request.session['user_secret'] = user_obj.secret
|
||||||
|
|
||||||
return render(request, 'login/scan.html', get_user_context(user_obj))
|
return render(request, 'login/scan.html', get_scan_context(request))
|
||||||
# return redirect('user/' + user_obj.secret)
|
|
||||||
|
|
||||||
|
# }}} callback #
|
||||||
def create_user(refresh_token, access_token, access_expires_in):
|
|
||||||
"""Create a User object based on information returned from Step 2 (callback
|
|
||||||
function) of auth flow.
|
|
||||||
|
|
||||||
:refresh_token: Used to renew access tokens.
|
|
||||||
:access_token: Used in Spotify API calls.
|
|
||||||
:access_expires_in: How long the access token last in seconds.
|
|
||||||
|
|
||||||
:returns: The newly created User object.
|
|
||||||
|
|
||||||
"""
|
|
||||||
profile_response = requests.get('https://api.spotify.com/v1/me',
|
|
||||||
headers={'Authorization': "Bearer " + access_token}).json()
|
|
||||||
user_id = profile_response['id']
|
|
||||||
|
|
||||||
try:
|
|
||||||
user_obj = User.objects.get(id=user_id)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
# Python docs recommends 32 bytes of randomness against brute
|
|
||||||
# force attacks
|
|
||||||
user_obj = User.objects.create(
|
|
||||||
id=user_id,
|
|
||||||
secret=secrets.token_urlsafe(32),
|
|
||||||
refresh_token=refresh_token,
|
|
||||||
access_token=access_token,
|
|
||||||
access_expires_in=access_expires_in,
|
|
||||||
)
|
|
||||||
|
|
||||||
return user_obj
|
|
||||||
|
|
||||||
# admin_graphs {{{ #
|
# admin_graphs {{{ #
|
||||||
|
|
||||||
@@ -136,7 +88,21 @@ def admin_graphs(request):
|
|||||||
"""
|
"""
|
||||||
user_id = "polarbier"
|
user_id = "polarbier"
|
||||||
# user_id = "chrisshyi13"
|
# user_id = "chrisshyi13"
|
||||||
|
|
||||||
|
request.session['user_id'] = user_id
|
||||||
|
# request.session['user_secret'] = user_obj.secret
|
||||||
|
request.session['user_secret'] = User.objects.get(id=user_id).secret
|
||||||
user_obj = User.objects.get(id=user_id)
|
user_obj = User.objects.get(id=user_id)
|
||||||
return render(request, 'graphs/logged_in.html', get_user_context(user_obj))
|
return render(request, 'graphs/logged_in.html', get_user_context(user_obj))
|
||||||
|
|
||||||
# }}} admin_graphs #
|
# }}} admin_graphs #
|
||||||
|
|
||||||
|
def upload_history(request):
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = HistoryUploadForm(request.POST, request.FILES)
|
||||||
|
if form.is_valid():
|
||||||
|
upload_obj = form.save()
|
||||||
|
# return redirect('graphs:display_history_table')
|
||||||
|
return redirect('api:import_history', upload_id=upload_obj.id)
|
||||||
|
|
||||||
|
return render(request, 'login/scan.html', get_scan_context(request))
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ astroid==1.6.3
|
|||||||
certifi==2018.4.16
|
certifi==2018.4.16
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
Django==2.0.5
|
Django==2.0.5
|
||||||
|
django-filter==2.0
|
||||||
djangorestframework==3.8.2
|
djangorestframework==3.8.2
|
||||||
|
django-tables2==2.0.2
|
||||||
idna==2.6
|
idna==2.6
|
||||||
isort==4.3.4
|
isort==4.3.4
|
||||||
lazy-object-proxy==1.3.1
|
lazy-object-proxy==1.3.1
|
||||||
mccabe==0.6.1
|
mccabe==0.6.1
|
||||||
psycopg2-binary==2.7.4
|
psycopg2-binary==2.7.4
|
||||||
pylint==1.8.4
|
pylint==1.8.4
|
||||||
|
python-dateutil==2.7.5
|
||||||
pytz==2018.4
|
pytz==2018.4
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
|
tablib==0.12.1
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ INSTALLED_APPS = [
|
|||||||
'login.apps.LoginConfig',
|
'login.apps.LoginConfig',
|
||||||
'api.apps.ApiConfig',
|
'api.apps.ApiConfig',
|
||||||
'graphs.apps.GraphsConfig',
|
'graphs.apps.GraphsConfig',
|
||||||
|
'django_tables2',
|
||||||
|
'django_filters',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@@ -128,3 +130,6 @@ STATIC_URL = '/static/'
|
|||||||
STATICFILES_DIRS = [
|
STATICFILES_DIRS = [
|
||||||
os.path.join(BASE_DIR, "static"),
|
os.path.join(BASE_DIR, "static"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
MEDIA_URL = '/media/'
|
||||||
|
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
body {
|
body {
|
||||||
background-color: #1e1e1e;
|
/* dark grey */
|
||||||
|
background-color: #1e1e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,p {
|
h1,th {
|
||||||
color: grey;
|
/* light grey */
|
||||||
|
color: #cccccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,td {
|
||||||
|
/* light-dark grey */
|
||||||
|
color: #b2b2b2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
update-history.sh
Executable file
1
update-history.sh
Executable file
@@ -0,0 +1 @@
|
|||||||
|
/home/kevin/coding/spotify-lib-vis/bin/python /home/kevin/coding/spotify-lib-vis/src/manage.py update-history >> /home/kevin/coding/spotify-lib-vis/src/api/management/commands/update-history.log
|
||||||
Reference in New Issue
Block a user