summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichaël Ball <michael.ball@gmail.com>2017-06-17 09:57:39 +0100
committerMichaël Ball <michael.ball@gmail.com>2017-06-17 09:57:39 +0100
commitda5c7e73112db917f2fb0c91635c48954cc3648e (patch)
tree59ec22239b1c3cd23ec8e843249c85b543f679d2
parentd06f96388d754ed41876f7fccb63f84241d44963 (diff)
parent3296708955e111579f00da8054ed2a4a86834766 (diff)
Merge branch 'feature/encoding-options' into develop
-rw-r--r--mach2.py162
-rw-r--r--models/user.py14
-rw-r--r--tests/mach2_test.py43
-rw-r--r--tests/testapp.dbbin2048 -> 2048 bytes
4 files changed, 192 insertions, 27 deletions
diff --git a/mach2.py b/mach2.py
index e1f186c..daf5027 100644
--- a/mach2.py
+++ b/mach2.py
@@ -1,5 +1,5 @@
+"""An app to serve a music library."""
import base64
-import json
import os
import subprocess
import sqlite3
@@ -7,8 +7,8 @@ import tempfile
import mimetypes
-from flask import (Blueprint, Flask, Response, current_app, g, redirect,
- render_template, request, url_for)
+from flask import (Blueprint, Flask, Response, abort, current_app, g, jsonify,
+ redirect, render_template, request, url_for)
from flask_compress import Compress
from flask_login import (LoginManager, current_user, login_required,
login_user, logout_user)
@@ -31,6 +31,7 @@ _LOGIN_MANAGER.login_view = "mach2.login"
def get_db():
+ """Get the application database."""
database = getattr(g, "_database", None)
if database is None:
database = sqlite3.connect(current_app.config["DATABASE"])
@@ -42,12 +43,14 @@ def get_db():
@MACH2.teardown_app_request
def close_connection(exception):
+ """Close the database connection."""
database = getattr(g, "_database", None)
if database is not None:
database.close()
def query_db(query, args=(), one=False):
+ """Query the application database."""
cur = get_db().execute(query, args)
result = cur.fetchall()
cur.close()
@@ -56,6 +59,12 @@ def query_db(query, args=(), one=False):
@_LOGIN_MANAGER.request_loader
def load_user_from_request(req):
+ """Load the user from the request.
+
+ Args:
+ req (flask.Request): The request object.
+
+ """
# first, try to login using the api_key url arg
api_key = req.args.get('api_key', None)
@@ -94,6 +103,7 @@ def load_user_from_request(req):
@MACH2.route("/")
@login_required
def index():
+ """Return the index."""
return render_template("index.html", user=current_user)
@@ -144,7 +154,7 @@ def albums():
for returned_album in returned_albums:
result_albums.append(returned_album.as_dict())
- return json.dumps(result_albums)
+ return jsonify(result_albums)
@MACH2.route("/albums/<int:album_id>/tracks")
@@ -156,7 +166,7 @@ def album_tracks(album_id):
for album_track in returned_album.tracks:
result_tracks.append(album_track.as_dict())
- return json.dumps(result_tracks)
+ return jsonify(result_tracks)
@MACH2.route("/albums/<int:album_id>/artists")
@@ -168,7 +178,7 @@ def album_artists(album_id):
for album_artist in returned_album.artists:
result_artists.append(album_artist.as_dict())
- return json.dumps(result_artists)
+ return jsonify(result_artists)
@MACH2.route("/albums/<int:album_id>")
@@ -176,7 +186,7 @@ def album_artists(album_id):
def album(album_id):
returned_album = Album(current_app.config["LIBRARY"], id=album_id)
- return json.dumps(returned_album.as_dict())
+ return jsonify(returned_album.as_dict())
@MACH2.route("/albums/<album_name>")
@@ -189,7 +199,7 @@ def album_search(album_name):
"operator": "LIKE"}):
result_albums.append(returned_album.as_dict())
- return json.dumps(result_albums)
+ return jsonify(result_albums)
@MACH2.route("/artists")
@@ -226,7 +236,7 @@ def artists():
for returned_artist in returned_artists:
result_artists.append(returned_artist.as_dict())
- return json.dumps(result_artists)
+ return jsonify(result_artists)
@MACH2.route("/artists/<int:artist_id>/tracks")
@@ -238,7 +248,7 @@ def artist_tracks(artist_id):
for artist_track in returned_artist.tracks:
result_tracks.append(artist_track.as_dict())
- return json.dumps(result_tracks)
+ return jsonify(result_tracks)
@MACH2.route("/artists/<int:artist_id>/albums")
@@ -250,7 +260,7 @@ def artist_albums(artist_id):
for artist_album in returned_artist.albums:
result_albums.append(artist_album.as_dict())
- return json.dumps(result_albums)
+ return jsonify(result_albums)
@MACH2.route("/artists/<int:artist_id>")
@@ -258,7 +268,7 @@ def artist_albums(artist_id):
def artist_info(artist_id):
artist = Artist(current_app.config["LIBRARY"], id=artist_id)
- return json.dumps(artist.as_dict())
+ return jsonify(artist.as_dict())
@MACH2.route("/artists/<artist_name>")
@@ -272,7 +282,7 @@ def artist_search(artist_name):
}):
result_artists.append(artist.as_dict())
- return json.dumps(artists)
+ return jsonify(artists)
@MACH2.route("/tracks")
@@ -308,7 +318,7 @@ def tracks():
for returned_track in returned_tracks:
result_tracks.append(returned_track.as_dict())
- return json.dumps(result_tracks)
+ return jsonify(result_tracks)
@MACH2.route("/tracks/<int:track_id>/artists")
@@ -320,7 +330,7 @@ def track_artists(track_id):
for track_artist in returned_track.artists:
result_artists.append(track_artist.as_dict())
- return json.dumps(result_artists)
+ return jsonify(result_artists)
@MACH2.route("/tracks/<int:track_id>")
@@ -375,20 +385,126 @@ def track_search(track_name):
"operator": "LIKE"}):
result_tracks.append(returned_track.as_dict())
- return json.dumps(result_tracks)
+ return jsonify(result_tracks)
+
+
+@MACH2.route("/user", defaults={"user_id": None},
+ methods=["GET", "POST", "PUT"])
+@MACH2.route("/user/<int:user_id>", methods=["DELETE", "GET", "PUT"])
+@login_required
+def users(user_id):
+ """Create, retrieve, update or delete a user.
+
+ Args:
+ user_id (obj:`int`, optional): The ID of the user. Defaults to None.
+ If none, and the request uses a GET or PUT method, this works
+ on the currently logged in user.
+
+ """
+ def update_user(user, user_data):
+ """Update the user with the supplied data.
+
+ Args:
+ user (obj:`User`): The user to update.
+ user_data (obj:`Dict[str, str]`): The data to update the user with.
+
+ """
+ db_conn = get_db()
+ if "password" in user_data:
+ password_hash, api_key = user.new_password(
+ user_data["password"])
+
+ update_query = ("UPDATE user SET password_hash = ?, "
+ "api_key = ? WHERE id = ?")
+
+ rows_updated = 0
+
+ with db_conn:
+ rows_updated = get_db().execute(
+ update_query, (password_hash, api_key, user.id))
+
+ if rows_updated > 0:
+ user.password_hash = password_hash
+ user.api_key = api_key
+ else:
+ error = dict(message="Unable to update user")
+ return jsonify(error), 500
+
+ if "transcode_command" in user_data:
+ update_query = ("UPDATE user SET transcode_command = ? "
+ "WHERE id = ?")
+
+ rows_updated = 0
+
+ with db_conn:
+ rows_updated = db_conn.execute(
+ update_query, (user_data["transcode_command"], user.id))
+
+ if rows_updated > 0:
+ user.transcode_command = user_data[
+ "transcode_command"]
+ else:
+ error = dict(message="Unable to update user")
+ return jsonify(error), 500
+
+ return jsonify(user.to_dict())
+
+ if user_id:
+ local_user = load_user(user_id)
+
+ if not local_user:
+ abort(404)
+
+ if request.method == "DELETE":
+ query = "DELETE FROM user WHERE user = ?"
+
+ get_db().execute(query, user_id)
+
+ return "", 204
+
+ elif request.method == "PUT":
+ user_data = request.get_json()
+
+ if not user_data:
+ abort(415)
+
+ return update_user(local_user, user_data)
+
+ else:
+ if request.method == "POST":
+
+ new_user_data = request.get_json()
+
+ if not new_user_data:
+ abort(415)
+
+ # TODO: create new user and return the object
+
+ local_user = load_user(current_user.id)
+
+ if request.method == "PUT":
+ user_data = request.get_json()
+
+ if not user_data:
+ abort(415)
+
+ return update_user(local_user, user_data)
+
+ return jsonify(local_user.to_dict())
@_LOGIN_MANAGER.user_loader
-def load_user(userid):
- user = None
- result = query_db("SELECT * FROM user WHERE id = ?", [userid], one=True)
+def load_user(user_id):
+ local_user = None
+ result = query_db("SELECT * FROM user WHERE id = ?", [user_id], one=True)
if result:
- user = User(id=result[0], username=result[1], password_hash=result[2],
- authenticated=1, active=result[4], anonymous=0,
- transcode_command=result[7])
+ local_user = User(
+ id=result[0], username=result[1],password_hash=result[2],
+ authenticated=1, active=result[4], anonymous=0,
+ transcode_command=result[7])
- return user
+ return local_user
@MACH2.route("/login", methods=["GET", "POST"])
diff --git a/models/user.py b/models/user.py
index ea90f39..4d4887f 100644
--- a/models/user.py
+++ b/models/user.py
@@ -1,8 +1,9 @@
+"""Define a user."""
from os import urandom
from itsdangerous import URLSafeTimedSerializer
-from common.security import pwd_context, secret_key
+from common.security import pwd_context
class User(object):
@@ -57,3 +58,14 @@ class User(object):
else:
raise ValueError("No user")
+
+ def to_dict(self):
+ """Return a dict representation of the user."""
+ return_dict = dict()
+ for key, item in self.__dict__.items():
+ try:
+ return_dict[key] = item.to_dict()
+ except AttributeError:
+ return_dict[key] = item
+
+ return return_dict
diff --git a/tests/mach2_test.py b/tests/mach2_test.py
index c33fd92..adee00d 100644
--- a/tests/mach2_test.py
+++ b/tests/mach2_test.py
@@ -1,4 +1,7 @@
+"""Tests for the mach2 app."""
import json
+import random
+import string
import unittest
import pytest
@@ -9,22 +12,33 @@ from mach2 import create_app
@pytest.mark.usefixtures("app")
class Mach2TestCase(unittest.TestCase):
+ """Provides tests for the mach2 app."""
def setUp(self):
+ """Set up the state before the tests run."""
app = create_app(database=self.db, library=self.library_db)
- app.config['TESTING'] = True
+ app.config["TESTING"] = True
self.app = app.test_client()
def login(self, username, password):
- return self.app.post('/login', data=dict(
+ """Log in to the app.
+
+ Args:
+ username (str): The username to log in with.
+ password (str): The password to log in with.
+
+ """
+ return self.app.post("/login", data=dict(
username=username,
password=password
), follow_redirects=True)
def logout(self):
- return self.app.get('/logout', follow_redirects=True)
+ """Log out of the app."""
+ return self.app.get("/logout", follow_redirects=True)
def test_login(self):
+ """Test logging in to the app."""
rv = self.login("admin", "testpass")
assert six.b("Log out") in rv.data
self.logout()
@@ -33,6 +47,7 @@ class Mach2TestCase(unittest.TestCase):
self.logout()
def test_album(self):
+ """Test retrieving albums."""
self.login("admin", "testpass")
rv = self.app.get("/albums/1")
@@ -41,6 +56,7 @@ class Mach2TestCase(unittest.TestCase):
self.logout()
def test_artists(self):
+ """Test retrieving artists."""
self.login("admin", "testpass")
rv = self.app.get("/artists")
@@ -51,3 +67,24 @@ class Mach2TestCase(unittest.TestCase):
assert artists
self.logout()
+
+ def test_encoding_options(self):
+ """Test setting encoding options."""
+ self.login("admin", "testpass")
+
+ transcode_string = "".join(
+ random.choice(
+ string.ascii_lowercase + string.digits) for _ in range(10))
+
+ transcode_command = dict(transcode_command=transcode_string)
+
+ put_response = self.app.put(
+ "/user", data=json.dumps(transcode_command),
+ content_type="application/json")
+
+ assert put_response.status_code == 200
+
+ get_response = self.app.get("/user")
+
+ user = json.loads(get_response.data.decode("utf-8"))
+ assert user["transcode_command"] == six.u(transcode_string)
diff --git a/tests/testapp.db b/tests/testapp.db
index 3ccfa2f..ae0702d 100644
--- a/tests/testapp.db
+++ b/tests/testapp.db
Binary files differ