Commit inicial
Habia trabajado un buen poco pero como vi que tenia que separar los repositorios perdi bastante la historia :c
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__
|
||||
venv
|
||||
.idea
|
||||
db.sqlite3
|
||||
197
DOCS.md
Normal file
197
DOCS.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# Documentación API
|
||||
|
||||
## Comunicación con MusicBrainz
|
||||
|
||||
Todos los links a continuación se mapean aproximadamente a lo que indica la documentación de
|
||||
[musicbrainz](https://musicbrainz.org/doc/Development/XML_Web_Service/Version_2) utilizando su formato
|
||||
de JSON
|
||||
|
||||
### Get
|
||||
|
||||
- `GET` `/api/brainz/get/artist/<mbid>/`
|
||||
|
||||
Obtiene artista por su musicbrainz id, se puede incluir un parametro `inc` el cual agregara entidades
|
||||
relacionados al artista, estos deben ser separados por símbolos `+`, las entidades disponibles son:
|
||||
- `recordings`
|
||||
- `releases`
|
||||
- `release-groups`
|
||||
Estas entidades relacionadas siempre estarán limitadas a 25.
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/get/artist/fa3b825f-7c85-4377-b393-d28a2016e293/?inc=releases+recordings
|
||||
|
||||
|
||||
- `GET` `/api/brainz/get/release-group/<mbid>`
|
||||
|
||||
Obtiene release-group por su musicbrainz id, se puede incluir un parametro `inc` el cual agregara entidades
|
||||
relacionados al release-group, estos deben ser separados por símbolos `+`, las entidades disponibles son:
|
||||
- `artists`
|
||||
- `releases`
|
||||
Estas entidades relacionadas siempre estarán limitadas a 25.
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/get/release-group/86ac3ebd-2fee-49f7-a249-ddc6553970dd/?inc=releases
|
||||
|
||||
- `GET` `/api/brainz/get/release/<mbid>`
|
||||
|
||||
Obtiene release por su musicbrainz id, se puede incluir un parametro `inc` el cual agregara entidades
|
||||
relacionados al release, estos deben ser separados por símbolos `+`, las entidades disponibles son:
|
||||
- `artists`
|
||||
- `release-groups`
|
||||
- `recordings`
|
||||
Estas entidades relacionadas siempre estarán limitadas a 25.
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/get/release/8c2b980a-dbc4-4047-b637-c470c29a6e05/?inc=recordings+artists
|
||||
|
||||
- `GET` `/api/brainz/get/recording/<mbid>`
|
||||
|
||||
Obtiene recording por su musicbrainz id, se puede incluir un parametro `inc` el cual agregara entidades
|
||||
relacionados al recording, estos deben ser separados por símbolos `+`, las entidades disponibles son:
|
||||
- `artists`
|
||||
- `releases`
|
||||
Estas entidades relacionadas siempre estarán limitadas a 25.
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/get/recording/051abadc-c78e-4fa1-a45e-1b24d6dab322/
|
||||
|
||||
### Search
|
||||
|
||||
- `GET` `/api/brainz/search/artist/`
|
||||
|
||||
Busca un artista en la base de datos de musicbrainz, los parámetros disponibles son los siguientes:
|
||||
- `query`: Parámetro obligatorio el cual es un string con el que buscar
|
||||
- `limit`: Parámetro opcional para limitar la cantidad de resultados, por default es 25
|
||||
- `offset`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/search/artist/?query=in%20love%20with%20a%20ghost
|
||||
|
||||
- `GET` `/api/brainz/search/release-group/`
|
||||
|
||||
Busca un release-group en la base de datos de musicbrainz, los parámetros disponibles son los siguientes:
|
||||
- `query`: Parametro obligatorio el cual es un string con el que buscar
|
||||
- `limit`: Parametro opcional para limitar la cantidad de resultados, por default es 25
|
||||
- `offset`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/search/release-group/?query=sizzlar&limit=1
|
||||
|
||||
- `GET` `/api/brainz/search/release/`
|
||||
|
||||
Busca un release en la base de datos de musicbrainz, los parámetros disponibles son los siguientes:
|
||||
- `query`: Parametro obligatorio el cual es un string con el que buscar
|
||||
- `limit`: Parametro opcional para limitar la cantidad de resultados, por default es 25
|
||||
- `offset`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/search/release/?query=no&limit=10&offset=30
|
||||
|
||||
- `GET` `/api/brainz/search/recording/`
|
||||
|
||||
Busca un release en la base de datos de musicbrainz, los parámetros disponibles son los siguientes:
|
||||
- `query`: Parametro obligatorio el cual es un string con el que buscar
|
||||
- `limit`: Parametro opcional para limitar la cantidad de resultados, por default es 25
|
||||
- `offset`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/search/recording/?query=no%20shade
|
||||
|
||||
### Browse
|
||||
|
||||
- `GET` `/api/brainz/browse/artist/`
|
||||
|
||||
Busca un artista dada una serie de parámetros, como por ejemplo, obtener un artista de la release
|
||||
efac54db-1ff9-4ca4-a220-df2b155f3eb9. Sus parámetros son los siguientes
|
||||
- `inc`: Parametro opcional para indicar los datos a incluir junto a los artistas encontrados, disponibles:
|
||||
- `aliases`
|
||||
- `limit`: Parametro opcional para indicar el limite de resultados, por default es 25
|
||||
- `offstet`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Las entidades con las que se puede buscar son las siguientes:
|
||||
- `release-group`
|
||||
- `release`
|
||||
- `recording`
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/browse/artist/?recording=f059b12d-4379-4c4b-93eb-3a1f8026aa3b&release-group=37159cad-447e-4a71-bac4-9ef27e07d35b
|
||||
|
||||
|
||||
- `GET` `/api/brainz/browse/release-group/`
|
||||
|
||||
Busca una release-group dada una serie de parámetros, como por ejemplo, obtener la release-group
|
||||
de la release efac54db-1ff9-4ca4-a220-df2b155f3eb9. Sus parámetros son los siguientes
|
||||
- `inc`: Parametro opcional para indicar los datos a incluir junto a las release-group encontradas, disponibles:
|
||||
- `artist-credits`
|
||||
- `limit`: Parametro opcional para indicar el limite de resultados, por default es 25
|
||||
- `offstet`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Las entidades con las que se puede buscar son las siguientes:
|
||||
- `artist`
|
||||
- `release`
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/browse/release-group/?release=67459555-dddd-3af4-9f45-b07ae5545caa
|
||||
|
||||
- `GET` `/api/brainz/browse/release/`
|
||||
|
||||
Busca una release dada una serie de parámetros, como por ejemplo, obtener las releases del artista
|
||||
fa3b825f-7c85-4377-b393-d28a2016e293. Sus parámetros son los siguientes
|
||||
- `inc`: Parametro opcional para indicar los datos a incluir junto a las release-group encontradas
|
||||
- `limit`: Parametro opcional para indicar el limite de resultados, por default es 25
|
||||
- `offstet`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Las entidades con las que se puede buscar son las siguientes:
|
||||
- `artist`
|
||||
- `release-group`
|
||||
- `recording`
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/browse/release/?artist=65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab
|
||||
|
||||
- `GET` `/api/brainz/browse/recording/`
|
||||
|
||||
Busca una recording dada una serie de parámetros, como por ejemplo, obtener las recordings del artista
|
||||
fa3b825f-7c85-4377-b393-d28a2016e293. Sus parámetros son los siguientes
|
||||
- `inc`: Parametro opcional para indicar los datos a incluir junto a las release-group encontradas
|
||||
- `limit`: Parametro opcional para indicar el limite de resultados, por default es 25
|
||||
- `offstet`: Parametro opcional para realizar paginado, por default es 0
|
||||
|
||||
Las entidades con las que se puede buscar son las siguientes:
|
||||
- `artist`
|
||||
- `release`
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/browse/recording/?artist=65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab&release=d2891587-1be3-3b43-b80e-d6afd960ad08
|
||||
|
||||
### Cover Art
|
||||
|
||||
- `GET` `/api/brainz/coverart/release-group/<mbid>/<int:size>/`
|
||||
|
||||
Busca el cover art de un release-group por su mbid, opcionalmente se le puede incluir el tamaño del cover art,
|
||||
que puede ser 250, 500, 1200
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/coverart/release-group/325b927d-72df-4e71-a5f9-7c717f72fb42/
|
||||
|
||||
- `GET` `/api/brainz/coverart/release/<mbid>/<int:size>/`
|
||||
|
||||
Busca el cover art de una release por su mbid, opcionalmente se le puede incluir el tamaño del cover art,
|
||||
que puede ser 250, 500, 1200
|
||||
|
||||
Ejemplo
|
||||
|
||||
https://musiclist.danielcortes.xyz/api/brainz/coverart/release/887ea51a-71de-4628-8f02-b061f6904a64/1200
|
||||
0
fetcher/__init__.py
Normal file
0
fetcher/__init__.py
Normal file
5
fetcher/apps.py
Normal file
5
fetcher/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class FetcherConfig(AppConfig):
|
||||
name = 'fetcher'
|
||||
344
fetcher/musicbrainz.py
Normal file
344
fetcher/musicbrainz.py
Normal file
@@ -0,0 +1,344 @@
|
||||
import logging
|
||||
import requests
|
||||
from ratelimit import limits, sleep_and_retry
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from utils import replace_key, sanitize_keys
|
||||
from utils.cache import Cache as cache
|
||||
|
||||
_headers = {'accept': 'application/json', 'user-agent': 'MusicList/1.0 (danielcortes.xyz)'}
|
||||
_mb_host = 'https://musicbrainz.org/ws/2'
|
||||
_ca_host = 'http://coverartarchive.org'
|
||||
|
||||
_log = logging.getLogger('musicbrainz')
|
||||
_log.addHandler(logging.NullHandler())
|
||||
|
||||
|
||||
@sleep_and_retry
|
||||
@limits(calls=1, period=1)
|
||||
def _do_request(url, allow_redirects=True):
|
||||
"""Does a request to a path and returns the dictionary representing the json response
|
||||
|
||||
If the response is 200 it will return the full dictionary of the json that the response
|
||||
contains, and if is a 307 code, it will create a dictionary containing the response code
|
||||
and the link where is redirecting, any other response code will be just appended to a
|
||||
dictionary and returning.
|
||||
|
||||
:param str url: URL where to do the request
|
||||
|
||||
:return: The whole response object
|
||||
:raises ValueError when user-agent isn't set
|
||||
:raises ValueError when url isn't set
|
||||
|
||||
:returns A dictionary with the request response
|
||||
it always has a status key with the status code by its value,
|
||||
the rest of the response will be contained in the 'response' key
|
||||
if the request didn't fail
|
||||
"""
|
||||
|
||||
if not _headers['user-agent']:
|
||||
raise ValueError('User Agent isn\'t set')
|
||||
if not url:
|
||||
raise ValueError('URL cant be empty')
|
||||
|
||||
_log.info(f'Doing request to "{url}" with headers {_headers}')
|
||||
r = requests.get(url, headers=_headers, allow_redirects=allow_redirects)
|
||||
|
||||
_log.info(f'Request returned with status code {r.status_code}')
|
||||
|
||||
if r.status_code == 200:
|
||||
response = r.json(object_hook=sanitize_keys)
|
||||
elif r.status_code == 307:
|
||||
response = {'link': r.headers['location']}
|
||||
elif r.status_code == 404:
|
||||
response = {'status': r.status_code, 'error': f'No encontrado'}
|
||||
elif r.status_code == 400:
|
||||
response = {'status': r.status_code, 'error': f'Query erronea'}
|
||||
else:
|
||||
response = {'status': r.status_code, 'error': f'Error desconocido de musicbrainz'}
|
||||
|
||||
return response
|
||||
|
||||
|
||||
def _get(entity_type, mbid, includes=None):
|
||||
"""Does a get entity query to musicbrainz
|
||||
|
||||
:param str entity_type: Type of the entity (artist, release, recording...)
|
||||
:param str mbid: MBID of the entity to get
|
||||
:param list includes: List of include parameters (recording, releases...)
|
||||
|
||||
:return: The JSON response
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
if includes is None:
|
||||
_log.info(f'Getting {entity_type} of mbid {mbid}')
|
||||
|
||||
return _do_request(f'{_mb_host}/{entity_type}/{mbid}')
|
||||
else:
|
||||
_log.info(f'Getting {entity_type} of mbid {mbid} including {includes}')
|
||||
_includes = quote('+'.join(includes))
|
||||
|
||||
return _do_request(f'{_mb_host}/{entity_type}/{mbid}?inc={_includes}')
|
||||
|
||||
|
||||
def _search(entity_type, query, limit=25, offset=0):
|
||||
"""Does a search of an entity to musicbrainz
|
||||
|
||||
:param str entity_type: Type of the entity (artist, release, recording...)
|
||||
:param str query: The search string
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
|
||||
:return: The JSON response
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
_log.info(f'Searching {entity_type} with query "{query}" at offset {offset} with limit of {limit}')
|
||||
|
||||
if limit >= 0 and offset >= 0:
|
||||
_query = urlencode({'query': query, 'limit': limit, 'offset': offset})
|
||||
else:
|
||||
_query = urlencode({'query': query})
|
||||
|
||||
return _do_request(f'{_mb_host}/{entity_type}/?{_query}')
|
||||
|
||||
|
||||
def _browse(entity_type, params, includes=None, limit=25, offset=0):
|
||||
"""Browses entities of a type with the given parameters
|
||||
|
||||
:param str entity_type: Type of the entity (artist, release, recording...)
|
||||
:param dict params: Dictionary with the parameters to do the search
|
||||
:param list includes: List of include parameters (recording, releases...)
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
|
||||
:return: The JSON response
|
||||
:rtype: dict
|
||||
|
||||
:raises ValueError if params is not a dictionary
|
||||
"""
|
||||
|
||||
if not isinstance(params, dict):
|
||||
raise ValueError('Params must be a dictionary')
|
||||
|
||||
if includes is None:
|
||||
_log.info(f'Browsing {entity_type} with parameters {params} at offset {offset} with limit of {limit}')
|
||||
_query = urlencode({**params, 'limit': limit, 'offset': offset})
|
||||
else:
|
||||
_log.info(f'Browsing {entity_type} with parameters {params} including {includes} at offset {offset} with limit of {limit}')
|
||||
_query = urlencode({**params, 'inc': '+'.join(includes), 'limit': limit, 'offset': offset})
|
||||
|
||||
return _do_request(f'{_mb_host}/{entity_type}?{_query}')
|
||||
|
||||
|
||||
def _ca(entity_type, mbid, size=None):
|
||||
"""Gets the url of the cover art of a release or release-group
|
||||
|
||||
:param str entity_type: Type of the entity, could be release or release-group
|
||||
:param str mbid: MBID of the entity of whom is the cover art
|
||||
:param int size: Optional size of the cover art, could be 250, 500 or 1200
|
||||
any other value will be ignored
|
||||
|
||||
:return: The url of the cover art
|
||||
"""
|
||||
|
||||
_log.info(f'Obtaining the cover art of the entity with type {entity_type} and mbid {mbid} with size {size}')
|
||||
|
||||
_url = f'{_ca_host}/{entity_type}/{mbid}/front'
|
||||
if size in (250, 500, 1200):
|
||||
_url += f'-{size}'
|
||||
|
||||
return _do_request(_url, allow_redirects=False)
|
||||
|
||||
|
||||
@cache
|
||||
def get_artist_by_mbid(mbid, includes=None):
|
||||
"""Get an artist by its mbid
|
||||
|
||||
:param str mbid: MBID of the artist
|
||||
:param list includes: List of include parameters
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _get('artist', mbid, includes)
|
||||
|
||||
|
||||
@cache
|
||||
def get_release_group_by_mbid(mbid, includes=None):
|
||||
"""Get a release group by its mbid
|
||||
|
||||
:param str mbid: MBID of the release group
|
||||
:param list includes: List of include parameters
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _get('release-group', mbid, includes)
|
||||
|
||||
|
||||
@cache
|
||||
def get_release_by_mbid(mbid, includes=None):
|
||||
"""Get a release by its mbid
|
||||
|
||||
:param str mbid: MBID of the release
|
||||
:param list includes: List of include parameters
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _get('release', mbid, includes)
|
||||
|
||||
|
||||
@cache
|
||||
def get_recording_by_mbid(mbid, includes=None):
|
||||
"""Get a recording by its mbid
|
||||
|
||||
:param str mbid: MBID of the recording
|
||||
:param list includes: List of include parameters
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _get('recording', mbid, includes)
|
||||
|
||||
|
||||
@cache
|
||||
def search_artist(query, limit=25, offset=0):
|
||||
"""Search an artist by a query string
|
||||
|
||||
:param str query: Query string
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
return _search('artist', query, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def search_release(query, limit=25, offset=0):
|
||||
"""Search a release by a query string
|
||||
|
||||
:param str query: Query string
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
return _search('release', query, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def search_release_group(query, limit=25, offset=0):
|
||||
"""Search a release group by a query string
|
||||
|
||||
:param str query: Query string
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
return _search('release-group', query, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def search_recording(query, limit=25, offset=0):
|
||||
"""Search a recording by a query string
|
||||
|
||||
:param str query: Query string
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
return _search('recording', query, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def browse_artists(params, includes=None, limit=25, offset=0):
|
||||
"""Browse an artist given some params
|
||||
|
||||
:param dict params: Parameters to do a search
|
||||
:param list includes: List of include parameters
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _browse('artist', params, includes, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def browse_recordings(params, includes=None, limit=25, offset=0):
|
||||
"""Browse through recordings given some params
|
||||
|
||||
:param dict params: Parameters to do a search
|
||||
:param list includes: List of include parameters
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _browse('recording', params, includes, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def browse_releases(params, includes=None, limit=25, offset=0):
|
||||
"""Browse through releases given some params
|
||||
|
||||
:param dict params: Parameters to do a search
|
||||
:param list includes: List of include parameters
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _browse('release', params, includes, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def browse_release_groups(params, includes=None, limit=25, offset=0):
|
||||
"""Browse through release groups given some params
|
||||
|
||||
:param dict params: Parameters to do a search
|
||||
:param list includes: List of include parameters
|
||||
:param int limit: Limit of the search, defaults to 25 and has a max of 100
|
||||
:param int offset: Offset of the search for paging purposes
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
if includes is None:
|
||||
includes = []
|
||||
|
||||
return _browse('release-group', params, includes, limit, offset)
|
||||
|
||||
|
||||
@cache
|
||||
def get_release_cover_art(mbid, size=None):
|
||||
"""Gets the url of the cover art of a release
|
||||
|
||||
:param str mbid: MBID of the release of whom is the cover art
|
||||
:param int size: Optional size of the cover art, could be 250, 500 or 1200
|
||||
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
return _ca('release', mbid, size)
|
||||
|
||||
|
||||
@cache
|
||||
def get_release_group_cover_art(mbid, size=None):
|
||||
"""Gets the url of the cover art of a release group
|
||||
|
||||
:param str mbid: MBID of the release group of whom is the cover art
|
||||
:param int size: Optional size of the cover art, could be 250, 500 or 1200
|
||||
|
||||
:return: dictionary with the response
|
||||
"""
|
||||
return _ca('release-group', mbid, size)
|
||||
44
fetcher/tests.py
Normal file
44
fetcher/tests.py
Normal file
@@ -0,0 +1,44 @@
|
||||
import redis
|
||||
from django.test import TestCase
|
||||
|
||||
from fetcher import musicbrainz
|
||||
|
||||
|
||||
class MusicBrainzTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
redis.Redis().flushall()
|
||||
|
||||
def test_can_do_basic_request(self):
|
||||
artist_url = 'https://musicbrainz.org/ws/2/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab'
|
||||
musicbrainz._do_request(artist_url)
|
||||
|
||||
def test_do_request_response_format(self):
|
||||
artist_url = 'https://musicbrainz.org/ws/2/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab'
|
||||
response = musicbrainz._do_request(artist_url)
|
||||
self.assertIsInstance(response, dict)
|
||||
self.assertEquals(response['status'], 200)
|
||||
self.assertIsInstance(response['response'], dict)
|
||||
|
||||
def test_do_request_input_validation(self):
|
||||
artist_url = 'https://musicbrainz.org/ws/2/artist/65f4f0c5-ef9e-490c-aee3-909e7ae6b2ab'
|
||||
self.assertRaises(ValueError, musicbrainz._do_request, None)
|
||||
self.assertRaises(ValueError, musicbrainz._do_request, '')
|
||||
musicbrainz._headers['user-agent'] = None
|
||||
self.assertRaises(ValueError, musicbrainz._do_request, artist_url)
|
||||
|
||||
def test_cache(self):
|
||||
def fun():
|
||||
return {}
|
||||
|
||||
musicbrainz.cache(fun)()
|
||||
self.assertTrue(redis.Redis().keys('*') is not None)
|
||||
self.assertEquals(musicbrainz.cache(fun)(), {})
|
||||
|
||||
redis.Redis().set('fun, [[], {}]', '{"test": true}')
|
||||
self.assertEquals(musicbrainz.cache(fun)(), {"test": True})
|
||||
|
||||
def fun(a, b):
|
||||
return {}
|
||||
|
||||
musicbrainz.cache(fun)('word', [''])
|
||||
24
fetcher/urls.py
Normal file
24
fetcher/urls.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
|
||||
urlpatterns = [
|
||||
path('get/artist/<mbid>/', views.get_artist_by_mbid),
|
||||
path('get/release-group/<mbid>/', views.get_release_group_by_mbid),
|
||||
path('get/release/<mbid>/', views.get_release_by_mbid),
|
||||
path('get/recording/<mbid>/', views.get_recording_by_mbid),
|
||||
|
||||
path('search/artist/', views.search_artist),
|
||||
path('search/release-group/', views.search_release_group),
|
||||
path('search/release/', views.search_release),
|
||||
path('search/recording/', views.search_recording),
|
||||
|
||||
path('browse/artist/', views.browse_artists),
|
||||
path('browse/release-group/', views.browse_release_groups),
|
||||
path('browse/release/', views.browse_releases),
|
||||
path('browse/recording/', views.browse_recordings),
|
||||
|
||||
path('coverart/release-group/<mbid>/<int:size>/', views.get_release_group_cover_art),
|
||||
path('coverart/release-group/<mbid>/', views.get_release_group_cover_art),
|
||||
path('coverart/release/<mbid>/<int:size>', views.get_release_cover_art),
|
||||
path('coverart/release/<mbid>/', views.get_release_group_cover_art),
|
||||
]
|
||||
127
fetcher/views.py
Normal file
127
fetcher/views.py
Normal file
@@ -0,0 +1,127 @@
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
|
||||
from . import musicbrainz as mb
|
||||
|
||||
|
||||
def _get_by_mbid(request, entity_type, mbid):
|
||||
includes = request.GET.get('inc', '').split('+')
|
||||
|
||||
if entity_type == 'artist':
|
||||
response = mb.get_artist_by_mbid(mbid, includes)
|
||||
elif entity_type == 'release-group':
|
||||
response = mb.get_release_group_by_mbid(mbid, includes)
|
||||
elif entity_type == 'release':
|
||||
response = mb.get_release_by_mbid(mbid, includes)
|
||||
elif entity_type == 'recording':
|
||||
response = mb.get_recording_by_mbid(mbid, includes)
|
||||
else:
|
||||
raise ValueError('Entity Type isn\'t valid')
|
||||
|
||||
if 'status' in response:
|
||||
return Response(response, status=response['status'])
|
||||
else:
|
||||
return Response(response)
|
||||
|
||||
|
||||
def _search(request, entity_type):
|
||||
query = request.GET.get('query', '')
|
||||
limit = request.GET.get('limit', 25)
|
||||
offset = request.GET.get('offset', 0)
|
||||
|
||||
if entity_type == 'artist':
|
||||
response = mb.search_artist(query, limit, offset)
|
||||
elif entity_type == 'release-group':
|
||||
response = mb.search_release_group(query, limit, offset)
|
||||
elif entity_type == 'release':
|
||||
response = mb.search_release(query, limit, offset)
|
||||
elif entity_type == 'recording':
|
||||
response = mb.search_recording(query, limit, offset)
|
||||
else:
|
||||
raise ValueError('Entity Type isn\'t valid')
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
def _browse(request, entity_type):
|
||||
includes = request.GET.get('inc', '').split('+')
|
||||
limit = request.GET.get('limit', 25)
|
||||
offset = request.GET.get('offset', 0)
|
||||
|
||||
params = request.GET.copy().dict()
|
||||
if 'inc' in params:
|
||||
del params['inc']
|
||||
if 'limit' in params:
|
||||
del params['limit']
|
||||
if 'offset' in params:
|
||||
del params['offset']
|
||||
|
||||
if entity_type == 'artist':
|
||||
response = mb.browse_artists(params, includes, limit, offset)
|
||||
elif entity_type == 'release-group':
|
||||
response = mb.browse_release_groups(params, includes, limit, offset)
|
||||
elif entity_type == 'release':
|
||||
response = mb.browse_releases(params, includes, limit, offset)
|
||||
elif entity_type == 'recording':
|
||||
response = mb.browse_recordings(params, includes, limit, offset)
|
||||
else:
|
||||
raise ValueError('Entity Type isn\'t valid')
|
||||
|
||||
return Response(response)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_artist_by_mbid(request, mbid): return _get_by_mbid(request, 'artist', mbid)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_release_group_by_mbid(request, mbid): return _get_by_mbid(request, 'release-group', mbid)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_release_by_mbid(request, mbid): return _get_by_mbid(request, 'release', mbid)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_recording_by_mbid(request, mbid): return _get_by_mbid(request, 'recording', mbid)
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def search_artist(request): return _search(request, 'artist')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def search_release_group(request): return _search(request, 'release-group')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def search_release(request): return _search(request, 'release')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def search_recording(request): return _search(request, 'recording')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def browse_artists(request): return _browse(request, 'artist')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def browse_release_groups(request): return _browse(request, 'release-group')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def browse_releases(request): return _browse(request, 'release')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def browse_recordings(request): return _browse(request, 'recording')
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_release_cover_art(request, mbid, size=None): return Response(mb.get_release_cover_art(mbid, size))
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
def get_release_group_cover_art(request, mbid, size=None):
|
||||
return Response(mb.get_release_group_cover_art(mbid, size))
|
||||
0
lists/__init__.py
Normal file
0
lists/__init__.py
Normal file
3
lists/admin.py
Normal file
3
lists/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
5
lists/apps.py
Normal file
5
lists/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ListsConfig(AppConfig):
|
||||
name = 'lists'
|
||||
0
lists/migrations/__init__.py
Normal file
0
lists/migrations/__init__.py
Normal file
51
lists/models.py
Normal file
51
lists/models.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.db.models import CharField, TextField, IntegerField
|
||||
from django.db.models import ForeignKey, ManyToManyField
|
||||
from django.db.models import CASCADE
|
||||
from django.db.models import Model
|
||||
|
||||
|
||||
class Entity(Model):
|
||||
ENTITY_TYPES = [
|
||||
('artist', 'Artista'),
|
||||
('release-group', 'Grupo de Lanzamientos'),
|
||||
('release', 'Lanzamiento'),
|
||||
('recording', 'Grabación')
|
||||
]
|
||||
mbid = CharField(max_length=36, primary_key=True)
|
||||
entity_type = CharField(max_length=25, choices=ENTITY_TYPES)
|
||||
|
||||
|
||||
class Tag(Model):
|
||||
user = ForeignKey(User, on_delete=CASCADE)
|
||||
name = CharField(max_length=50)
|
||||
|
||||
|
||||
class ListItem(Model):
|
||||
user = ForeignKey(User, on_delete=CASCADE)
|
||||
entity = ForeignKey(Entity, on_delete=CASCADE)
|
||||
tags = ManyToManyField(Tag, on_delete=CASCADE)
|
||||
|
||||
|
||||
class Stars(Model):
|
||||
user = ForeignKey(User, on_delete=CASCADE)
|
||||
entity = ForeignKey(ListItem, on_delete=CASCADE)
|
||||
quantity = IntegerField()
|
||||
|
||||
|
||||
class Opinion(Model):
|
||||
user = ForeignKey(User, on_delete=CASCADE)
|
||||
entity = ForeignKey(ListItem, on_delete=CASCADE)
|
||||
opinion_text = TextField()
|
||||
|
||||
|
||||
class OpinionHelpful(Model):
|
||||
VOTES = [
|
||||
('Y', 'Si'),
|
||||
('N', 'No'),
|
||||
('F', 'Divertida')
|
||||
]
|
||||
|
||||
user = ForeignKey(User, on_delete=CASCADE)
|
||||
opinion = ForeignKey(Opinion, on_delete=CASCADE)
|
||||
vote = CharField(max_length=1, choices=VOTES)
|
||||
3
lists/tests.py
Normal file
3
lists/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
lists/views.py
Normal file
3
lists/views.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
21
manage.py
Executable file
21
manage.py
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'musiclist.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
0
musiclist/__init__.py
Normal file
0
musiclist/__init__.py
Normal file
16
musiclist/asgi.py
Normal file
16
musiclist/asgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
ASGI config for musiclist project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'musiclist.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
172
musiclist/settings.py
Normal file
172
musiclist/settings.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""
|
||||
Django settings for musiclist project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 3.0.5.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.0/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
# noinspection SpellCheckingInspection
|
||||
SECRET_KEY = 'c8%4_^4oc%wwqlcxlon-(_7v!xj8fbyba+pj*xy$oi*6#n!7ez'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'oauth2_provider',
|
||||
'corsheaders',
|
||||
'rest_framework',
|
||||
|
||||
'fetcher.apps.FetcherConfig',
|
||||
'users.apps.UsersConfig',
|
||||
'welcome.apps.WelcomeConfig',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'musiclist.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'musiclist.wsgi.application'
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/3.0/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.AllowAny',
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/3.0/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'es-es'
|
||||
TIME_ZONE = 'America/Santiago'
|
||||
USE_I18N = True
|
||||
USE_L10N = True
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/3.0/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
LOGGING_LEVEL = 'DEBUG'
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"coloredlogs": {
|
||||
"()": "coloredlogs.ColoredFormatter",
|
||||
"fmt": "[%(asctime)s] %(name)s %(levelname)s %(message)s",
|
||||
},
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"level": LOGGING_LEVEL,
|
||||
"formatter": "coloredlogs",
|
||||
},
|
||||
},
|
||||
"filters": {
|
||||
"hostname": {
|
||||
"()": "coloredlogs.HostNameFilter",
|
||||
},
|
||||
},
|
||||
"loggers": {
|
||||
"": {
|
||||
"handlers": ["console"],
|
||||
"level": LOGGING_LEVEL
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
CUSTOM_CACHE = {
|
||||
'enabled': True
|
||||
}
|
||||
|
||||
LOGIN_URL = '/auth/login/'
|
||||
AUTH_USER_MODEL = 'users.User'
|
||||
|
||||
CORS_ORIGIN_ALLOW_ALL = True
|
||||
10
musiclist/urls.py
Normal file
10
musiclist/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
path('', include('welcome.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
path('oauth/', include('oauth2_provider.urls', namespace='oauth2_provider')),
|
||||
path('auth/', include('users.urls')),
|
||||
path('api/brainz/', include('fetcher.urls')),
|
||||
]
|
||||
16
musiclist/wsgi.py
Normal file
16
musiclist/wsgi.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for musiclist project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/3.0/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'musiclist.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
django
|
||||
django-cors-middleware
|
||||
django-oauth-toolkit
|
||||
djangorestframework
|
||||
coloredlogs
|
||||
pygments
|
||||
ratelimit
|
||||
redis
|
||||
requests
|
||||
sphinx
|
||||
blake3
|
||||
0
users/__init__.py
Normal file
0
users/__init__.py
Normal file
88
users/admin.py
Normal file
88
users/admin.py
Normal file
@@ -0,0 +1,88 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.forms import ReadOnlyPasswordHashField
|
||||
from django.contrib.auth.models import Group
|
||||
from django import forms
|
||||
|
||||
from users.models import User, SocialNetworks
|
||||
|
||||
admin.site.unregister(Group)
|
||||
|
||||
|
||||
class UserAddForm(forms.ModelForm):
|
||||
password = forms.CharField(label='Contraseña', widget=forms.PasswordInput)
|
||||
password_confirmation = forms.CharField(label='Confirmación de contraseña', widget=forms.PasswordInput)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'email')
|
||||
|
||||
def clean_password_confirmation(self):
|
||||
password = self.cleaned_data.get("password")
|
||||
password_confirmation = self.cleaned_data.get("password_confirmation")
|
||||
|
||||
if password and password_confirmation and password != password_confirmation:
|
||||
raise forms.ValidationError('Las contraseñas no coinciden')
|
||||
|
||||
return password_confirmation
|
||||
|
||||
def save(self, commit=True):
|
||||
user = super().save(commit=False)
|
||||
user.set_password(self.cleaned_data['password'])
|
||||
|
||||
if commit:
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
class UserChangeForm(forms.ModelForm):
|
||||
password = ReadOnlyPasswordHashField()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('username', 'email', 'password', 'is_admin')
|
||||
|
||||
def clean_password(self):
|
||||
return self.initial['password']
|
||||
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
form = UserChangeForm
|
||||
add_form = UserAddForm
|
||||
|
||||
list_display = ('username', 'email', 'is_admin', 'is_active')
|
||||
list_filter = ('is_admin', 'is_active')
|
||||
|
||||
fieldsets = (
|
||||
(None, {'fields': ('username', 'email', 'password')}),
|
||||
('Permisos', {'fields': ('is_admin',)}),
|
||||
)
|
||||
|
||||
add_fieldsets = (
|
||||
(None, {'fields': ('username', 'email', 'password', 'password_confirmation')}),
|
||||
)
|
||||
|
||||
search_fields = ('username', 'email')
|
||||
ordering = ('username', 'email')
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
if not obj:
|
||||
return self.add_fieldsets
|
||||
return super().get_fieldsets(request, obj)
|
||||
|
||||
def get_form(self, request, obj=None, **kwargs):
|
||||
defaults = {}
|
||||
if obj is None:
|
||||
defaults['form'] = self.add_form
|
||||
defaults.update(kwargs)
|
||||
return super().get_form(request, obj, **defaults)
|
||||
|
||||
|
||||
@admin.register(SocialNetworks)
|
||||
class SocialNetworksAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'twitter', 'facebook', 'instagram', 'youtube', 'twitch')
|
||||
raw_id_fields = ('user',)
|
||||
|
||||
search_fields = ('user__username',)
|
||||
ordering = ('user',)
|
||||
5
users/apps.py
Normal file
5
users/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class UsersConfig(AppConfig):
|
||||
name = 'users'
|
||||
34
users/migrations/0001_initial.py
Normal file
34
users/migrations/0001_initial.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-16 02:58
|
||||
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.manager
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('username', models.CharField(max_length=40, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()])),
|
||||
('email', models.EmailField(max_length=254)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('is_admin', models.BooleanField(default=False)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('object', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
]
|
||||
56
users/migrations/0002_auto_20200516_0201.py
Normal file
56
users/migrations/0002_auto_20200516_0201.py
Normal file
@@ -0,0 +1,56 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-16 06:01
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SocialNetworks',
|
||||
fields=[
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
|
||||
('twitter', models.CharField(max_length=255, verbose_name='twitter')),
|
||||
('facebook', models.CharField(max_length=255, verbose_name='facebook')),
|
||||
('instagram', models.CharField(max_length=255, verbose_name='instagram')),
|
||||
('youtube', models.CharField(max_length=255, verbose_name='youtube')),
|
||||
('twitch', models.CharField(max_length=255, verbose_name='twitch')),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='user',
|
||||
options={'verbose_name': 'usuario', 'verbose_name_plural': 'usuarios'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='email',
|
||||
field=models.EmailField(max_length=254, verbose_name='correo'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, verbose_name='esta activo'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='is_admin',
|
||||
field=models.BooleanField(default=False, verbose_name='es administrador'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='password',
|
||||
field=models.CharField(max_length=128, verbose_name='contraseña'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='user',
|
||||
name='username',
|
||||
field=models.CharField(max_length=40, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='nombre de usuario'),
|
||||
),
|
||||
]
|
||||
42
users/migrations/0003_auto_20200516_0225.py
Normal file
42
users/migrations/0003_auto_20200516_0225.py
Normal file
@@ -0,0 +1,42 @@
|
||||
# Generated by Django 3.0.6 on 2020-05-16 06:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0002_auto_20200516_0201'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='socialnetworks',
|
||||
options={'verbose_name': 'Redes Sociales', 'verbose_name_plural': 'Redes Sociales'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='socialnetworks',
|
||||
name='facebook',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='facebook'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='socialnetworks',
|
||||
name='instagram',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='instagram'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='socialnetworks',
|
||||
name='twitch',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='twitch'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='socialnetworks',
|
||||
name='twitter',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='twitter'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='socialnetworks',
|
||||
name='youtube',
|
||||
field=models.CharField(blank=True, max_length=255, verbose_name='youtube'),
|
||||
),
|
||||
]
|
||||
78
users/models.py
Normal file
78
users/models.py
Normal file
@@ -0,0 +1,78 @@
|
||||
from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager
|
||||
from django.contrib.auth.validators import UnicodeUsernameValidator
|
||||
from django.db import models
|
||||
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
def create_user(self, username, email, password):
|
||||
if not username:
|
||||
raise ValueError('El usuario debe tener un nombre de usuario')
|
||||
if not email:
|
||||
raise ValueError('El usuario debe tener un email')
|
||||
if not password:
|
||||
raise ValueError('El usuario debe tener una contraseña')
|
||||
|
||||
user = self.model(
|
||||
username=self.model.normalize_username(username),
|
||||
email=self.normalize_email(email),
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def create_superuser(self, username, email, password=None):
|
||||
user = self.create_user(
|
||||
username=username,
|
||||
email=email,
|
||||
password=password,
|
||||
)
|
||||
user.is_admin = True
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
|
||||
class User(AbstractBaseUser):
|
||||
class Meta:
|
||||
verbose_name = 'usuario'
|
||||
verbose_name_plural = 'usuarios'
|
||||
|
||||
object = UserManager()
|
||||
|
||||
username_validator = UnicodeUsernameValidator()
|
||||
|
||||
username = models.CharField('nombre de usuario', max_length=40, validators=[username_validator], unique=True)
|
||||
email = models.EmailField('correo')
|
||||
is_active = models.BooleanField('esta activo', default=True)
|
||||
is_admin = models.BooleanField('es administrador', default=False)
|
||||
password = models.CharField('contraseña', max_length=128)
|
||||
|
||||
USERNAME_FIELD = 'username'
|
||||
EMAIL_FIELD = 'email'
|
||||
|
||||
REQUIRED_FIELDS = ['email']
|
||||
|
||||
def has_perm(self, *args, **kwargs):
|
||||
return True
|
||||
|
||||
def has_module_perms(self, *args, **kwargs):
|
||||
return True
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
return self.is_admin
|
||||
|
||||
|
||||
class SocialNetworks(models.Model):
|
||||
class Meta:
|
||||
verbose_name = 'Redes Sociales'
|
||||
verbose_name_plural = 'Redes Sociales'
|
||||
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, primary_key=True)
|
||||
twitter = models.CharField('twitter', max_length=255, blank=True)
|
||||
facebook = models.CharField('facebook', max_length=255, blank=True)
|
||||
instagram = models.CharField('instagram', max_length=255, blank=True)
|
||||
youtube = models.CharField('youtube', max_length=255, blank=True)
|
||||
twitch = models.CharField('twitch', max_length=255, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f'{self._meta.verbose_name_plural} de {self.user.username}'
|
||||
88
users/static/users/css/base.css
Normal file
88
users/static/users/css/base.css
Normal file
@@ -0,0 +1,88 @@
|
||||
:root {
|
||||
--background-color: hsl(10, 20%, 98%);
|
||||
--foreground-color: hsl(10, 10%, 13%);
|
||||
--primary-color: hsl(284, 30%, 40%);
|
||||
--highlight-color: hsl(290, 86%, 43%);
|
||||
--light-color: hsl(10, 10%, 40%);
|
||||
--error-color: hsl(0, 100%, 60%)
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
overflow-y: scroll;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--highlight-color)
|
||||
}
|
||||
|
||||
.login-box {
|
||||
margin: 100px auto;
|
||||
width: 45ch;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-box .heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--primary-color);
|
||||
color: var(--background-color);
|
||||
height: 3em;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-box .form {
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-box .form a {
|
||||
display: inline-block;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: var(--error-color);
|
||||
border: 1px rgba(234, 234, 234, .4) solid;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.error p {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: .3em;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
input[type='text'], input[type='password'], input[type='email'] {
|
||||
display: block;
|
||||
padding: 8px;
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 1.3em;
|
||||
font-size: .9em;
|
||||
}
|
||||
|
||||
.submit {
|
||||
display: block;
|
||||
width: 10ch;
|
||||
height: 2.3em;
|
||||
margin: .4em auto;
|
||||
border: 1px solid rgba(234, 234, 234, .3);
|
||||
border-radius: 8px;
|
||||
color: var(--background-color);
|
||||
background-color: var(--primary-color);
|
||||
font-size: .9em;
|
||||
}
|
||||
4
users/static/users/css/normalize.css
vendored
Normal file
4
users/static/users/css/normalize.css
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/******************************************************************************
|
||||
=> NORMALIZE.CSS
|
||||
*******************************************************************************/
|
||||
button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}
|
||||
20
users/templates/users/base.html
Normal file
20
users/templates/users/base.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<title>{% block title %}MusicList{% endblock %}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "users/css/normalize.css" %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static "users/css/base.css" %}">
|
||||
{% block extrastyle %}{% endblock %}
|
||||
{% block extrahead %}{% endblock %}
|
||||
{% block responsive %}
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
{% endblock %}
|
||||
{% block blockbots %}<meta name="robots" content="NONE,NOARCHIVE">{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
29
users/templates/users/login.html
Normal file
29
users/templates/users/login.html
Normal file
@@ -0,0 +1,29 @@
|
||||
{% extends "users/base.html" %}
|
||||
{% block content %}
|
||||
<div class="login-box">
|
||||
<div class="heading">
|
||||
<p>MusicList Login</p>
|
||||
</div>
|
||||
<div class="form">
|
||||
{% if error %}
|
||||
<div class="error">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="{% url 'auth:login' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
|
||||
<label for="username">Usuario:</label>
|
||||
<input type="text" name="username" id="username" required>
|
||||
|
||||
<label for="password">Contraseña:</label>
|
||||
<input type="password" name="password" id="password" required>
|
||||
|
||||
<button class="submit" type="submit">Entrar</button>
|
||||
</form>
|
||||
<a href="{% url 'auth:register' %}?next={{ next|urlencode }}">¿No tienes cuenta?</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
33
users/templates/users/register.html
Normal file
33
users/templates/users/register.html
Normal file
@@ -0,0 +1,33 @@
|
||||
{% extends "users/base.html" %}
|
||||
{% block content %}
|
||||
<div class="login-box">
|
||||
<div class="heading">
|
||||
<p>MusicList Registration</p>
|
||||
</div>
|
||||
<div class="form">
|
||||
{% if error %}
|
||||
<div class="error">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="POST" action="{% url 'auth:register' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
|
||||
<label for="username">Usuario:</label>
|
||||
<input type="text" name="username" id="username" {% if old.username %} value="{{ old.username }}" {% endif%} required>
|
||||
|
||||
<label for="email">Email:</label>
|
||||
<input type="email" name="email" id="email" {% if old.email %} value="{{ old.email}}" {% endif%}required>
|
||||
|
||||
<label for="password">Contraseña:</label>
|
||||
<input type="password" name="password" id="password" required>
|
||||
|
||||
<label for="password_confirm">Confirme Contraseña:</label>
|
||||
<input type="password" name="password_confirm" id="password_confirm" required>
|
||||
|
||||
<button class="submit" type="submit">Entrar</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
10
users/urls.py
Normal file
10
users/urls.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from django.urls import path
|
||||
from users import views
|
||||
|
||||
app_name = 'auth'
|
||||
|
||||
urlpatterns = [
|
||||
path('login/', views.login, name='login'),
|
||||
path('logout/', views.logout, name='logout'),
|
||||
path('register/', views.register, name='register')
|
||||
]
|
||||
102
users/views.py
Normal file
102
users/views.py
Normal file
@@ -0,0 +1,102 @@
|
||||
from django.http import HttpResponseRedirect, HttpResponseNotAllowed
|
||||
from django.shortcuts import render
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.contrib.auth import authenticate, login as auth_login, logout as auth_logout, get_user_model
|
||||
|
||||
|
||||
def get_next_url(request):
|
||||
next_url = request.POST.get('next', request.GET.get('next', ''))
|
||||
|
||||
url_is_safe = url_has_allowed_host_and_scheme(
|
||||
url=next_url,
|
||||
allowed_hosts=request.get_host(),
|
||||
require_https=request.is_secure(),
|
||||
)
|
||||
|
||||
print(next_url if url_is_safe else '/')
|
||||
return next_url if url_is_safe else '/'
|
||||
|
||||
|
||||
def login(request):
|
||||
if request.method == 'GET':
|
||||
return _login_get(request)
|
||||
elif request.method == 'POST':
|
||||
return _login_post(request)
|
||||
else:
|
||||
return HttpResponseNotAllowed(permitted_methods=['GET', 'POST'])
|
||||
|
||||
|
||||
def _login_get(request):
|
||||
return render(request, template_name='users/login.html', context={'next': get_next_url(request)})
|
||||
|
||||
|
||||
def _login_post(request):
|
||||
username = request.POST.get('username', '')
|
||||
password = request.POST.get('password', '')
|
||||
|
||||
user = authenticate(request, username=username, password=password)
|
||||
|
||||
if user is not None:
|
||||
auth_login(request, user)
|
||||
return HttpResponseRedirect(get_next_url(request))
|
||||
else:
|
||||
return render(
|
||||
request,
|
||||
template_name='users/login.html',
|
||||
context={'next': get_next_url(request), 'error': 'Usuario o contraseña son incorrectos'}
|
||||
)
|
||||
|
||||
|
||||
def logout(request):
|
||||
auth_logout(request)
|
||||
return HttpResponseRedirect(get_next_url(request))
|
||||
|
||||
|
||||
def register(request):
|
||||
if request.method == 'GET':
|
||||
return _register_get(request)
|
||||
elif request.method == 'POST':
|
||||
return _register_post(request)
|
||||
else:
|
||||
return HttpResponseNotAllowed(permitted_methods=['GET', 'POST'])
|
||||
|
||||
|
||||
def _register_get(request):
|
||||
return render(request, template_name='users/register.html', context={'next': get_next_url(request)})
|
||||
|
||||
|
||||
def _register_post(request):
|
||||
username = request.POST.get('username', '')
|
||||
password = request.POST.get('password', '')
|
||||
password_confirm = request.POST.get('password_confirm', '')
|
||||
email = request.POST.get('email', '')
|
||||
|
||||
old = {
|
||||
'username': username,
|
||||
'email': email
|
||||
}
|
||||
|
||||
if not username:
|
||||
return render(
|
||||
request,
|
||||
template_name='users/register.html',
|
||||
context={'next': get_next_url(request), 'error': 'Debe ingresar un nombre de usuario', 'old': old}
|
||||
)
|
||||
|
||||
if get_user_model().objects.filter(username=username).count() > 0:
|
||||
return render(
|
||||
request,
|
||||
template_name='users/register.html',
|
||||
context={'next': get_next_url(request), 'error': 'El nombre de usuario esta en uso', 'old': old}
|
||||
)
|
||||
|
||||
if password and password != password_confirm:
|
||||
return render(
|
||||
request,
|
||||
template_name='users/register.html',
|
||||
context={'next': get_next_url(request), 'error': 'Las contraseñas no coinciden', 'old': old}
|
||||
)
|
||||
|
||||
user = get_user_model().objects.create_user(username, email, password)
|
||||
auth_login(request, user)
|
||||
return HttpResponseRedirect(get_next_url(request))
|
||||
71
utils/__init__.py
Normal file
71
utils/__init__.py
Normal file
@@ -0,0 +1,71 @@
|
||||
import logging
|
||||
from collections import Mapping
|
||||
|
||||
_log = logging.getLogger('utils')
|
||||
_log.addHandler(logging.NullHandler())
|
||||
|
||||
|
||||
def pretty_print_json(json_input):
|
||||
"""Formats and prints json to stdout with colors using pygments"""
|
||||
import json
|
||||
|
||||
from pygments import highlight
|
||||
from pygments.lexers import JsonLexer
|
||||
from pygments.formatters import TerminalTrueColorFormatter
|
||||
|
||||
formatted_json = json.dumps(json_input, indent=2)
|
||||
print(highlight(formatted_json, JsonLexer(), TerminalTrueColorFormatter()))
|
||||
|
||||
|
||||
def message_response(status, error_message):
|
||||
"""Sends an error response with the status code of the error and an explanation"""
|
||||
from django.http import JsonResponse
|
||||
|
||||
json_message = {
|
||||
'status_code': status,
|
||||
'message': error_message
|
||||
}
|
||||
|
||||
return JsonResponse(json_message, status=status)
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
def require_JSON(function):
|
||||
"""Decorator to make a view only accept a json body"""
|
||||
|
||||
import functools
|
||||
import json
|
||||
|
||||
@functools.wraps(function)
|
||||
def decorator(*args, **kwargs):
|
||||
try:
|
||||
received_json = json.loads(args[0].body)
|
||||
return function(*args, **kwargs, received_json=received_json)
|
||||
except json.JSONDecodeError as error:
|
||||
_log.warning(f'Function {function.__name__} got a non json request body')
|
||||
return message_response(400, 'Se envío json no valido')
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def expected_keys(keys, dictionary):
|
||||
for key in keys:
|
||||
if key not in dictionary:
|
||||
return f'No se encuentra {key}'
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def replace_key(json, old, new):
|
||||
json[new] = json[old]
|
||||
del json[old]
|
||||
|
||||
|
||||
def sanitize_keys(json):
|
||||
for key in list(json.keys()):
|
||||
if '-' in key and key in json:
|
||||
new_key = key.replace('-', '_')
|
||||
replace_key(json, key, new_key)
|
||||
return json
|
||||
|
||||
|
||||
54
utils/cache.py
Normal file
54
utils/cache.py
Normal file
@@ -0,0 +1,54 @@
|
||||
import json
|
||||
import logging
|
||||
from blake3 import blake3
|
||||
from redis import Redis
|
||||
from django.conf import settings
|
||||
|
||||
_log = logging.getLogger('cache')
|
||||
_log.addHandler(logging.NullHandler())
|
||||
|
||||
_redis = Redis()
|
||||
|
||||
|
||||
class Cache:
|
||||
""" Decorator that caches the result of a function
|
||||
|
||||
It works by generating a key given the function signature and uses it to store the result
|
||||
of the function on redis, so if the function is called with the same parameters again it
|
||||
will be cached and will retrieve the data from redis and will not execute the function.
|
||||
|
||||
It is assumed that the function will returns a dictionary that can be formatted to json.
|
||||
"""
|
||||
|
||||
"""If caching is enabled"""
|
||||
enabled = settings.CUSTOM_CACHE['enabled']
|
||||
|
||||
"""Time to expire, by default a week"""
|
||||
expire = 60 * 60 * 24 * 7
|
||||
|
||||
def __init__(self, function):
|
||||
self.function = function
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
if not self.enabled:
|
||||
_log.info('Cache is disabled, executing function directly')
|
||||
return self.function(*args, **kwargs)
|
||||
|
||||
_log.info(f'Caching function {self.function.__name__} with argument list {args} and dictionary {kwargs}')
|
||||
|
||||
key = f'{self.function.__name__}:{self.create_hash_key(args, kwargs)}'
|
||||
_log.debug(f'Resolved key for function is "{key}"')
|
||||
|
||||
if _redis.exists(key):
|
||||
_log.info(f'Key was in cache')
|
||||
result = json.loads(_redis.get(key))
|
||||
return result
|
||||
else:
|
||||
_log.info('Key was not in cache')
|
||||
result = self.function(*args, **kwargs)
|
||||
_redis.set(key, json.dumps(result), ex=self.expire)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def create_hash_key(args, kwargs):
|
||||
return blake3((str(args) + str(kwargs)).encode('utf-8')).hexdigest()
|
||||
0
welcome/__init__.py
Normal file
0
welcome/__init__.py
Normal file
5
welcome/apps.py
Normal file
5
welcome/apps.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WelcomeConfig(AppConfig):
|
||||
name = 'welcome'
|
||||
29
welcome/static/welcome/css/base.css
Normal file
29
welcome/static/welcome/css/base.css
Normal file
@@ -0,0 +1,29 @@
|
||||
:root {
|
||||
--background-color: hsl(10, 20%, 98%);
|
||||
--foreground-color: hsl(10, 10%, 13%);
|
||||
--primary-color: hsl(284, 30%, 40%);
|
||||
--highlight-color: hsl(290, 86%, 43%);
|
||||
--light-color: hsl(10, 10%, 40%);
|
||||
--error-color: hsl(0, 100%, 60%)
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
color: var(--foreground-color);
|
||||
overflow-y: scroll;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
max-width: 80ch;
|
||||
margin: 25vh auto;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--highlight-color)
|
||||
}
|
||||
|
||||
4
welcome/static/welcome/css/normalize.css
vendored
Normal file
4
welcome/static/welcome/css/normalize.css
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/******************************************************************************
|
||||
=> NORMALIZE.CSS
|
||||
*******************************************************************************/
|
||||
button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none}
|
||||
22
welcome/templates/welcome/index.html
Normal file
22
welcome/templates/welcome/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<title>{% block title %}MusicList{% endblock %}</title>
|
||||
<link rel="stylesheet" type="text/css" href="{% static "welcome/css/normalize.css" %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static "welcome/css/base.css" %}">
|
||||
{% block extrastyle %}{% endblock %}
|
||||
{% block extrahead %}{% endblock %}
|
||||
{% block responsive %}
|
||||
<meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Hey no deberías estar aquí!</h1>
|
||||
<p>Esto esta en desarrollo, esta es la api para una sexy aplicación con la que se podrá organizar musica
|
||||
muy al estilo de <a href="https://www.myanimelist.net">MyAnimeList</a> pero claro, con musica.</p>
|
||||
<p>Algún dia estará terminada :3</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
11
welcome/urls.py
Normal file
11
welcome/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.shortcuts import render
|
||||
from django.urls import path
|
||||
|
||||
|
||||
def index(request):
|
||||
return render(request, 'welcome/index.html')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path('', index)
|
||||
]
|
||||
Reference in New Issue
Block a user