diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6e4761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,129 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..98ce324 --- /dev/null +++ b/Pipfile @@ -0,0 +1,22 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +django = "*" +psycopg2-binary = "*" +django-environ = "*" +django-on-heroku = "*" +gunicorn = "*" +django-registration-redux = "*" +django-extensions = "*" +django-debug-toolbar = "*" +djangorestframework = "*" +pygments = "*" +django-filter = "*" + +[dev-packages] + +[requires] +python_version = "3.10" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..99d6011 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,209 @@ +{ + "_meta": { + "hash": { + "sha256": "1e6d4448496d283cd2d0239a665e4fefff4b4ad208037a2de2b83185ad274e01" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asgiref": { + "hashes": [ + "sha256:1d2880b792ae8757289136f1db2b7b99100ce959b2aa57fd69dab783d05afac4", + "sha256:4a29362a6acebe09bf1d6640db38c1dc3d9217c68e6f9f6204d72667fc19a424" + ], + "markers": "python_version >= '3.7'", + "version": "==3.5.2" + }, + "dj-database-url": { + "hashes": [ + "sha256:ccf3e8718f75ddd147a1e212fca88eecdaa721759ee48e38b485481c77bca3dc", + "sha256:cd354a3b7a9136d78d64c17b2aec369e2ae5616fbca6bfbe435ef15bb372ce39" + ], + "version": "==1.0.0" + }, + "django": { + "hashes": [ + "sha256:a153ffd5143bf26a877bfae2f4ec736ebd8924a46600ca089ad96b54a1d4e28e", + "sha256:acb21fac9275f9972d81c7caf5761a89ec3ea25fe74545dd26b8a48cb3a0203e" + ], + "index": "pypi", + "version": "==4.1.1" + }, + "django-debug-toolbar": { + "hashes": [ + "sha256:95fc2fd29c56cc86678aae9f6919ececefe892f2a78c4004b193a223a8380c3d", + "sha256:fe7fe3f21865218827e2162ecc06eba386dfe8cffe4f3501c49bb4359e06a0e6" + ], + "index": "pypi", + "version": "==3.6.0" + }, + "django-environ": { + "hashes": [ + "sha256:bff5381533056328c9ac02f71790bd5bf1cea81b1beeb648f28b81c9e83e0a21", + "sha256:f21a5ef8cc603da1870bbf9a09b7e5577ab5f6da451b843dbcc721a7bca6b3d9" + ], + "index": "pypi", + "version": "==0.9.0" + }, + "django-extensions": { + "hashes": [ + "sha256:2a4f4d757be2563cd1ff7cfdf2e57468f5f931cc88b23cf82ca75717aae504a4", + "sha256:421464be390289513f86cb5e18eb43e5dc1de8b4c27ba9faa3b91261b0d67e09" + ], + "index": "pypi", + "version": "==3.2.1" + }, + "django-filter": { + "hashes": [ + "sha256:ed429e34760127e3520a67f415bec4c905d4649fbe45d0d6da37e6ff5e0287eb", + "sha256:ed473b76e84f7e83b2511bb2050c3efb36d135207d0128dfe3ae4b36e3594ba5" + ], + "index": "pypi", + "version": "==22.1" + }, + "django-on-heroku": { + "hashes": [ + "sha256:4a72ade056335112cba22eea332552d840b718dd8140d43c2d70bb739875955b", + "sha256:a41fe83ef7ecb022ca92b2950a61f79fef156e58f4f35fc7fc4ecacdc5225fb8" + ], + "index": "pypi", + "version": "==1.1.2" + }, + "django-registration-redux": { + "hashes": [ + "sha256:5079dd36980cc0faddf91a6e991129680410611b1059d8154d064cc0146744b2", + "sha256:88eb98530d98a7e3451bf728c0a5f6fe7ea2f45c65ef18f619ef37b940c854f5" + ], + "index": "pypi", + "version": "==2.11" + }, + "djangorestframework": { + "hashes": [ + "sha256:0c33407ce23acc68eca2a6e46424b008c9c02eceb8cf18581921d0092bc1f2ee", + "sha256:24c4bf58ed7e85d1fe4ba250ab2da926d263cd57d64b03e8dcef0ac683f8b1aa" + ], + "index": "pypi", + "version": "==3.13.1" + }, + "gunicorn": { + "hashes": [ + "sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e", + "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8" + ], + "index": "pypi", + "version": "==20.1.0" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:01310cf4cf26db9aea5158c217caa92d291f0500051a6469ac52166e1a16f5b7", + "sha256:083a55275f09a62b8ca4902dd11f4b33075b743cf0d360419e2051a8a5d5ff76", + "sha256:090f3348c0ab2cceb6dfbe6bf721ef61262ddf518cd6cc6ecc7d334996d64efa", + "sha256:0a29729145aaaf1ad8bafe663131890e2111f13416b60e460dae0a96af5905c9", + "sha256:0c9d5450c566c80c396b7402895c4369a410cab5a82707b11aee1e624da7d004", + "sha256:10bb90fb4d523a2aa67773d4ff2b833ec00857f5912bafcfd5f5414e45280fb1", + "sha256:12b11322ea00ad8db8c46f18b7dfc47ae215e4df55b46c67a94b4effbaec7094", + "sha256:152f09f57417b831418304c7f30d727dc83a12761627bb826951692cc6491e57", + "sha256:15803fa813ea05bef089fa78835118b5434204f3a17cb9f1e5dbfd0b9deea5af", + "sha256:15c4e4cfa45f5a60599d9cec5f46cd7b1b29d86a6390ec23e8eebaae84e64554", + "sha256:183a517a3a63503f70f808b58bfbf962f23d73b6dccddae5aa56152ef2bcb232", + "sha256:1f14c8b0942714eb3c74e1e71700cbbcb415acbc311c730370e70c578a44a25c", + "sha256:1f6b813106a3abdf7b03640d36e24669234120c72e91d5cbaeb87c5f7c36c65b", + "sha256:280b0bb5cbfe8039205c7981cceb006156a675362a00fe29b16fbc264e242834", + "sha256:2d872e3c9d5d075a2e104540965a1cf898b52274a5923936e5bfddb58c59c7c2", + "sha256:2f9ffd643bc7349eeb664eba8864d9e01f057880f510e4681ba40a6532f93c71", + "sha256:3303f8807f342641851578ee7ed1f3efc9802d00a6f83c101d21c608cb864460", + "sha256:35168209c9d51b145e459e05c31a9eaeffa9a6b0fd61689b48e07464ffd1a83e", + "sha256:3a79d622f5206d695d7824cbf609a4f5b88ea6d6dab5f7c147fc6d333a8787e4", + "sha256:404224e5fef3b193f892abdbf8961ce20e0b6642886cfe1fe1923f41aaa75c9d", + "sha256:46f0e0a6b5fa5851bbd9ab1bc805eef362d3a230fbdfbc209f4a236d0a7a990d", + "sha256:47133f3f872faf28c1e87d4357220e809dfd3fa7c64295a4a148bcd1e6e34ec9", + "sha256:526ea0378246d9b080148f2d6681229f4b5964543c170dd10bf4faaab6e0d27f", + "sha256:53293533fcbb94c202b7c800a12c873cfe24599656b341f56e71dd2b557be063", + "sha256:539b28661b71da7c0e428692438efbcd048ca21ea81af618d845e06ebfd29478", + "sha256:57804fc02ca3ce0dbfbef35c4b3a4a774da66d66ea20f4bda601294ad2ea6092", + "sha256:63638d875be8c2784cfc952c9ac34e2b50e43f9f0a0660b65e2a87d656b3116c", + "sha256:6472a178e291b59e7f16ab49ec8b4f3bdada0a879c68d3817ff0963e722a82ce", + "sha256:68641a34023d306be959101b345732360fc2ea4938982309b786f7be1b43a4a1", + "sha256:6e82d38390a03da28c7985b394ec3f56873174e2c88130e6966cb1c946508e65", + "sha256:761df5313dc15da1502b21453642d7599d26be88bff659382f8f9747c7ebea4e", + "sha256:7af0dd86ddb2f8af5da57a976d27cd2cd15510518d582b478fbb2292428710b4", + "sha256:7b1e9b80afca7b7a386ef087db614faebbf8839b7f4db5eb107d0f1a53225029", + "sha256:874a52ecab70af13e899f7847b3e074eeb16ebac5615665db33bce8a1009cf33", + "sha256:887dd9aac71765ac0d0bac1d0d4b4f2c99d5f5c1382d8b770404f0f3d0ce8a39", + "sha256:8b344adbb9a862de0c635f4f0425b7958bf5a4b927c8594e6e8d261775796d53", + "sha256:8fc53f9af09426a61db9ba357865c77f26076d48669f2e1bb24d85a22fb52307", + "sha256:91920527dea30175cc02a1099f331aa8c1ba39bf8b7762b7b56cbf54bc5cce42", + "sha256:93cd1967a18aa0edd4b95b1dfd554cf15af657cb606280996d393dadc88c3c35", + "sha256:99485cab9ba0fa9b84f1f9e1fef106f44a46ef6afdeec8885e0b88d0772b49e8", + "sha256:9d29409b625a143649d03d0fd7b57e4b92e0ecad9726ba682244b73be91d2fdb", + "sha256:a29b3ca4ec9defec6d42bf5feb36bb5817ba3c0230dd83b4edf4bf02684cd0ae", + "sha256:a9e1f75f96ea388fbcef36c70640c4efbe4650658f3d6a2967b4cc70e907352e", + "sha256:accfe7e982411da3178ec690baaceaad3c278652998b2c45828aaac66cd8285f", + "sha256:adf20d9a67e0b6393eac162eb81fb10bc9130a80540f4df7e7355c2dd4af9fba", + "sha256:af9813db73395fb1fc211bac696faea4ca9ef53f32dc0cfa27e4e7cf766dcf24", + "sha256:b1c8068513f5b158cf7e29c43a77eb34b407db29aca749d3eb9293ee0d3103ca", + "sha256:bda845b664bb6c91446ca9609fc69f7db6c334ec5e4adc87571c34e4f47b7ddb", + "sha256:c381bda330ddf2fccbafab789d83ebc6c53db126e4383e73794c74eedce855ef", + "sha256:c3ae8e75eb7160851e59adc77b3a19a976e50622e44fd4fd47b8b18208189d42", + "sha256:d1c1b569ecafe3a69380a94e6ae09a4789bbb23666f3d3a08d06bbd2451f5ef1", + "sha256:def68d7c21984b0f8218e8a15d514f714d96904265164f75f8d3a70f9c295667", + "sha256:dffc08ca91c9ac09008870c9eb77b00a46b3378719584059c034b8945e26b272", + "sha256:e3699852e22aa68c10de06524a3721ade969abf382da95884e6a10ff798f9281", + "sha256:e847774f8ffd5b398a75bc1c18fbb56564cda3d629fe68fd81971fece2d3c67e", + "sha256:ffb7a888a047696e7f8240d649b43fb3644f14f0ee229077e7f6b9f9081635bd" + ], + "index": "pypi", + "version": "==2.9.3" + }, + "pygments": { + "hashes": [ + "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1", + "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42" + ], + "index": "pypi", + "version": "==2.13.0" + }, + "pytz": { + "hashes": [ + "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197", + "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5" + ], + "version": "==2022.2.1" + }, + "setuptools": { + "hashes": [ + "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82", + "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57" + ], + "markers": "python_version >= '3.7'", + "version": "==65.3.0" + }, + "sqlparse": { + "hashes": [ + "sha256:0c00730c74263a94e5a9919ade150dfc3b19c574389985446148402998287dae", + "sha256:48719e356bb8b42991bdbb1e8b83223757b93789c00910a616a071910ca4a64d" + ], + "markers": "python_version >= '3.5'", + "version": "==0.4.2" + }, + "whitenoise": { + "hashes": [ + "sha256:8e9c600a5c18bd17655ef668ad55b5edf6c24ce9bdca5bf607649ca4b1e8e2c2", + "sha256:8fa943c6d4cd9e27673b70c21a07b0aa120873901e099cd46cab40f7cc96d567" + ], + "markers": "python_version >= '3.7'", + "version": "==6.2.0" + } + }, + "develop": {} +} diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..d0ae9bc --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: gunicorn config.wsgi +release: python manage.py migrate \ No newline at end of file diff --git a/README.md b/README.md index b095002..e8fcb89 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,25 @@ Create a new API-only application that lets users keep track of books, including important information like title, author, publication date, a genre, and a field that marks it as "featured". Books should be unique by title and author (that is, you can't have two books with the same title _and_ author; two books with the same title is fine as long as the authors are different). +1. users track books +2. book has title, author, publication date, genre, featured +3. constraint for book by title and author(title can be same if author is different) + Users should be able to search for a book by title or author. +1. sort book by title or author + Anyone can add a new book as long as the same book is not already in the library. Only admin users can update book details (like whether it is "featured") and delete books. +1. users can create a book if not already in library +2. admin can update book details(featured) and delete books + You'll also need a book tracking model so that users can mark a book as "want to read", "reading", or "read/done"; this status can also be updated. The tracking model should have a foreign key to a book and to a user. +1. tracking model has status and foreign keys to books and to users Optionally users can take notes on books. These notes have a foreign key relationship with a book and a user, a datetime they are created, a note body, a boolean field marking it as public or private, and an optional page number. Private notes are viewable only by the author. When notes are retrieved, return them by creation time in reverse order. +1. Users make notes on books - created time, public/private setting, page number optional +2. Retreive by reverse date Users should be able to see a list of all the books they are tracking, or a list by status (for instance, all their "want to read" books). You _could_ consider using [DjangoFilterBackend](https://www.django-rest-framework.org/api-guide/filtering/#djangofilterbackend) for this. diff --git a/config/__init__.py b/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/config/__pycache__/__init__.cpython-310.pyc b/config/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..d4d9ff9 Binary files /dev/null and b/config/__pycache__/__init__.cpython-310.pyc differ diff --git a/config/__pycache__/settings.cpython-310.pyc b/config/__pycache__/settings.cpython-310.pyc new file mode 100644 index 0000000..ae9e009 Binary files /dev/null and b/config/__pycache__/settings.cpython-310.pyc differ diff --git a/config/__pycache__/urls.cpython-310.pyc b/config/__pycache__/urls.cpython-310.pyc new file mode 100644 index 0000000..a925c81 Binary files /dev/null and b/config/__pycache__/urls.cpython-310.pyc differ diff --git a/config/asgi.py b/config/asgi.py new file mode 100644 index 0000000..9502b7f --- /dev/null +++ b/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config 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/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_asgi_application() diff --git a/config/settings.py b/config/settings.py new file mode 100644 index 0000000..f4005ad --- /dev/null +++ b/config/settings.py @@ -0,0 +1,158 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 4.1.1. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" + +from pathlib import Path +import environ +import django_on_heroku + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + +env = environ.Env( + DEBUG=(bool, False) +) + +environ.Env.read_env() + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = env('SECRET_KEY') +# SECRET_KEY = 'django-insecure-=r-hy4df_#0kr4te97+7&3q9t+^+krxv&*oo4lh!_^m3ocw3ux' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = env('DEBUG') +# DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + # 'registration', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'django_filters', + 'django_extensions', + 'rest_framework', + 'library_api', + # 'debug_toolbar', + +] + +MIDDLEWARE = [ + # 'debug_toolbar.middleware.DebugToolbarMiddleware', + '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 = 'config.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['templates'], + '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 = 'config.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + 'default': env.db(), + # 'default': { + # 'ENGINE': 'django.db.backends.sqlite3', + # 'NAME': BASE_DIR / 'db.sqlite3', + # } +} + +django_on_heroku.settings(locals()) +del DATABASES['default']['OPTIONS']['sslmode'] + +# Password validation +# https://docs.djangoproject.com/en/4.1/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', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'US/Eastern' + +USE_I18N = True + +USE_TZ = True + +INTERNAL_IPS = [ + '127.0.0.1', +] + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = 'static/' +STATICFILES_DIRS = [ + BASE_DIR / 'static/' +] + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +AUTH_USER_MODEL = "library_api.CustomUser" +# SIMPLE_BACKEND_REDIRECT_URL = '/' +# LOGIN_REDIRECT_URL = '/' + +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10 +} \ No newline at end of file diff --git a/config/urls.py b/config/urls.py new file mode 100644 index 0000000..23244c6 --- /dev/null +++ b/config/urls.py @@ -0,0 +1,25 @@ +"""config URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api-auth/', include('rest_framework.urls')), + path('', include('library_api.urls')), + # path('accounts/', include('registration.backends.simple.urls')), + # path('__debug__/', include('debug_toolbar.urls')), +] diff --git a/config/wsgi.py b/config/wsgi.py new file mode 100644 index 0000000..3d2dc45 --- /dev/null +++ b/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config 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/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings') + +application = get_wsgi_application() diff --git a/genres.py b/genres.py new file mode 100644 index 0000000..ea6f3c5 --- /dev/null +++ b/genres.py @@ -0,0 +1,34 @@ +genres = [ + "Adventure", + "Art / Photography", + "Biography", + "Contemporary", + "Cookbook", + "Children's", + "Crafts / Hobbies", + "Development", + "Dystopian", + "Families & Relationships", + "Fantasy", + "Fiction", + "Guide / How-to", + "Health", + "History", + "Historical Fiction", + "Horror", + "Humor", + "Memoir / Autobiography", + "Motivational", + "Mystery", + "Non-Fiction", + "Paranormal", + "Poetry", + "Religious", + "Romance", + "Self-help", + "Science Fiction", + "Thriller", + "Travel", + "True Crime", + "Western", +] diff --git a/library_api/__init__.py b/library_api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_api/__pycache__/__init__.cpython-310.pyc b/library_api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..bffd4b9 Binary files /dev/null and b/library_api/__pycache__/__init__.cpython-310.pyc differ diff --git a/library_api/__pycache__/admin.cpython-310.pyc b/library_api/__pycache__/admin.cpython-310.pyc new file mode 100644 index 0000000..c65efe8 Binary files /dev/null and b/library_api/__pycache__/admin.cpython-310.pyc differ diff --git a/library_api/__pycache__/apps.cpython-310.pyc b/library_api/__pycache__/apps.cpython-310.pyc new file mode 100644 index 0000000..f6478d4 Binary files /dev/null and b/library_api/__pycache__/apps.cpython-310.pyc differ diff --git a/library_api/__pycache__/models.cpython-310.pyc b/library_api/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..8baea3a Binary files /dev/null and b/library_api/__pycache__/models.cpython-310.pyc differ diff --git a/library_api/__pycache__/permissions.cpython-310.pyc b/library_api/__pycache__/permissions.cpython-310.pyc new file mode 100644 index 0000000..07e8014 Binary files /dev/null and b/library_api/__pycache__/permissions.cpython-310.pyc differ diff --git a/library_api/__pycache__/serializers.cpython-310.pyc b/library_api/__pycache__/serializers.cpython-310.pyc new file mode 100644 index 0000000..c1088a5 Binary files /dev/null and b/library_api/__pycache__/serializers.cpython-310.pyc differ diff --git a/library_api/__pycache__/urls.cpython-310.pyc b/library_api/__pycache__/urls.cpython-310.pyc new file mode 100644 index 0000000..19e5938 Binary files /dev/null and b/library_api/__pycache__/urls.cpython-310.pyc differ diff --git a/library_api/__pycache__/views.cpython-310.pyc b/library_api/__pycache__/views.cpython-310.pyc new file mode 100644 index 0000000..082ca08 Binary files /dev/null and b/library_api/__pycache__/views.cpython-310.pyc differ diff --git a/library_api/admin.py b/library_api/admin.py new file mode 100644 index 0000000..5a694c9 --- /dev/null +++ b/library_api/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import CustomUser, Book, Track, Note + +class CustomUserAdmin(UserAdmin): + model = CustomUser + list_display = ["email", "username",] + +admin.site.register(CustomUser, CustomUserAdmin) +admin.site.register(Book) +admin.site.register(Track) +admin.site.register(Note) diff --git a/library_api/apps.py b/library_api/apps.py new file mode 100644 index 0000000..cb7c44f --- /dev/null +++ b/library_api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LibraryApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'library_api' diff --git a/library_api/migrations/0001_initial.py b/library_api/migrations/0001_initial.py new file mode 100644 index 0000000..63aa69a --- /dev/null +++ b/library_api/migrations/0001_initial.py @@ -0,0 +1,44 @@ +# Generated by Django 4.1 on 2022-09-13 18:52 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='CustomUser', + fields=[ + ('id', models.BigAutoField(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')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/library_api/migrations/0002_book_track_note_book_unique_title_author.py b/library_api/migrations/0002_book_track_note_book_unique_title_author.py new file mode 100644 index 0000000..5f0d202 --- /dev/null +++ b/library_api/migrations/0002_book_track_note_book_unique_title_author.py @@ -0,0 +1,51 @@ +# Generated by Django 4.1 on 2022-09-13 22:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_api', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(help_text='Enter a book title', max_length=100)), + ('author', models.CharField(help_text='Enter the Author', max_length=100)), + ('publication_date', models.DateField(blank=True, null=True)), + ('genre', models.CharField(help_text='Enter the Genre', max_length=100)), + ('featured', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Track', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('WR', 'Want to read'), ('RG', 'Reading'), ('RD', 'Read')], default='WR', max_length=100)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='track_books', to='library_api.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='track_users', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateField(auto_now_add=True)), + ('note', models.TextField(blank=True, help_text='Write your notes here', max_length=200, null=True)), + ('private', models.BooleanField(default=True)), + ('page', models.PositiveIntegerField(blank=True, null=True)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='note_books', to='library_api.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='note_users', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddConstraint( + model_name='book', + constraint=models.UniqueConstraint(fields=('title', 'author'), name='unique_title_author'), + ), + ] diff --git a/library_api/migrations/0003_alter_note_created_at.py b/library_api/migrations/0003_alter_note_created_at.py new file mode 100644 index 0000000..8e0c572 --- /dev/null +++ b/library_api/migrations/0003_alter_note_created_at.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.1 on 2022-09-15 00:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_api', '0002_book_track_note_book_unique_title_author'), + ] + + operations = [ + migrations.AlterField( + model_name='note', + name='created_at', + field=models.DateTimeField(auto_now_add=True), + ), + ] diff --git a/library_api/migrations/0004_alter_track_status.py b/library_api/migrations/0004_alter_track_status.py new file mode 100644 index 0000000..7944af3 --- /dev/null +++ b/library_api/migrations/0004_alter_track_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2022-09-15 18:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_api', '0003_alter_note_created_at'), + ] + + operations = [ + migrations.AlterField( + model_name='track', + name='status', + field=models.CharField(choices=[('WR', 'Want to read'), ('Reading', 'Reading'), ('RD', 'Read')], default='WR', max_length=100), + ), + ] diff --git a/library_api/migrations/0005_alter_track_status.py b/library_api/migrations/0005_alter_track_status.py new file mode 100644 index 0000000..073cec6 --- /dev/null +++ b/library_api/migrations/0005_alter_track_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1 on 2022-09-15 18:46 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_api', '0004_alter_track_status'), + ] + + operations = [ + migrations.AlterField( + model_name='track', + name='status', + field=models.CharField(choices=[('Want to read', 'Want to read'), ('Reading', 'Reading'), ('Read', 'Read')], default='Want to read', max_length=100), + ), + ] diff --git a/library_api/migrations/0006_alter_note_book_alter_note_user_alter_track_book_and_more.py b/library_api/migrations/0006_alter_note_book_alter_note_user_alter_track_book_and_more.py new file mode 100644 index 0000000..7997ba0 --- /dev/null +++ b/library_api/migrations/0006_alter_note_book_alter_note_user_alter_track_book_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.1 on 2022-09-16 18:59 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_api', '0005_alter_track_status'), + ] + + operations = [ + migrations.AlterField( + model_name='note', + name='book', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='library_api.book'), + ), + migrations.AlterField( + model_name='note', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='track', + name='book', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to='library_api.book'), + ), + migrations.AlterField( + model_name='track', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tracks', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/library_api/migrations/0007_track_unique_user.py b/library_api/migrations/0007_track_unique_user.py new file mode 100644 index 0000000..8cccf4d --- /dev/null +++ b/library_api/migrations/0007_track_unique_user.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.1 on 2022-09-18 00:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_api', '0006_alter_note_book_alter_note_user_alter_track_book_and_more'), + ] + + operations = [ + migrations.AddConstraint( + model_name='track', + constraint=models.UniqueConstraint(fields=('user', 'book'), name='unique_user'), + ), + ] diff --git a/library_api/migrations/__init__.py b/library_api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_api/migrations/__pycache__/0001_initial.cpython-310.pyc b/library_api/migrations/__pycache__/0001_initial.cpython-310.pyc new file mode 100644 index 0000000..b51b3c9 Binary files /dev/null and b/library_api/migrations/__pycache__/0001_initial.cpython-310.pyc differ diff --git a/library_api/migrations/__pycache__/0002_book_track_note_book_unique_title_author.cpython-310.pyc b/library_api/migrations/__pycache__/0002_book_track_note_book_unique_title_author.cpython-310.pyc new file mode 100644 index 0000000..a3acf6e Binary files /dev/null and b/library_api/migrations/__pycache__/0002_book_track_note_book_unique_title_author.cpython-310.pyc differ diff --git a/library_api/migrations/__pycache__/0003_alter_note_created_at.cpython-310.pyc b/library_api/migrations/__pycache__/0003_alter_note_created_at.cpython-310.pyc new file mode 100644 index 0000000..e394eaf Binary files /dev/null and b/library_api/migrations/__pycache__/0003_alter_note_created_at.cpython-310.pyc differ diff --git a/library_api/migrations/__pycache__/0004_alter_track_status.cpython-310.pyc b/library_api/migrations/__pycache__/0004_alter_track_status.cpython-310.pyc new file mode 100644 index 0000000..fa8cdf5 Binary files /dev/null and b/library_api/migrations/__pycache__/0004_alter_track_status.cpython-310.pyc differ diff --git a/library_api/migrations/__pycache__/0005_alter_track_status.cpython-310.pyc b/library_api/migrations/__pycache__/0005_alter_track_status.cpython-310.pyc new file mode 100644 index 0000000..4d56683 Binary files /dev/null and b/library_api/migrations/__pycache__/0005_alter_track_status.cpython-310.pyc differ diff --git a/library_api/migrations/__pycache__/0006_alter_note_book_alter_note_user_alter_track_book_and_more.cpython-310.pyc b/library_api/migrations/__pycache__/0006_alter_note_book_alter_note_user_alter_track_book_and_more.cpython-310.pyc new file mode 100644 index 0000000..14a680c Binary files /dev/null and b/library_api/migrations/__pycache__/0006_alter_note_book_alter_note_user_alter_track_book_and_more.cpython-310.pyc differ diff --git a/library_api/migrations/__pycache__/__init__.cpython-310.pyc b/library_api/migrations/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..bd3e7c5 Binary files /dev/null and b/library_api/migrations/__pycache__/__init__.cpython-310.pyc differ diff --git a/library_api/models.py b/library_api/models.py new file mode 100644 index 0000000..ebb8f41 --- /dev/null +++ b/library_api/models.py @@ -0,0 +1,61 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser +from django.db.models.constraints import UniqueConstraint + + +class CustomUser(AbstractUser): + pass + + def __str__(self): + return self.username + + +class Book(models.Model): + title = models.CharField(max_length=100, help_text='Enter a book title') + author = models.CharField(max_length=100, help_text='Enter the Author') + publication_date = models.DateField(blank=True, null=True) + genre = models.CharField(max_length=100, help_text='Enter the Genre') + featured = models.BooleanField(default=False) + + class Meta: + constraints = [ + UniqueConstraint(fields=['title', 'author'], name='unique_title_author') + ] + + def __str__(self): + return self.title + + +class Track(models.Model): + WANT = 'Want to read' + READING = 'Reading' + READ = 'Read' + STATUS_CHOICES = [ + (WANT, 'Want to read'), + (READING, 'Reading'), + (READ, 'Read'), + ] + user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='tracks') + book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='tracks') + status = models.CharField(max_length=100, choices=STATUS_CHOICES, default=WANT) + + class Meta: + constraints = [ + UniqueConstraint(fields=["user", "book"], name="unique_user") + ] + + def __str__(self): + return f'{self.status} {self.book}' + + +class Note(models.Model): + user = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='notes') + book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='notes') + created_at = models.DateTimeField(auto_now_add=True) + note = models.TextField(max_length=200, blank=True, null=True, help_text='Write your notes here') + private = models.BooleanField(default=True) + page = models.PositiveIntegerField(blank=True, null=True) + + + def __str__(self): + return self.note diff --git a/library_api/permissions.py b/library_api/permissions.py new file mode 100644 index 0000000..ce44d76 --- /dev/null +++ b/library_api/permissions.py @@ -0,0 +1,29 @@ +from rest_framework import permissions + + +class IsOwnerOrReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + if request.user.is_authenticated: + return True + return False + + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + if obj.user == request.user: + return True + return False + + +class IsAdminOrReadOnly(permissions.BasePermission): + def has_permission(self, request, view): + if request.user.is_authenticated: + return True + return False + + def has_object_permission(self, request, view, obj): + if request.method in permissions.SAFE_METHODS: + return True + if request.user.is_staff: + return True + return False diff --git a/library_api/serializers.py b/library_api/serializers.py new file mode 100644 index 0000000..b060e27 --- /dev/null +++ b/library_api/serializers.py @@ -0,0 +1,27 @@ +from django.contrib.auth.models import User +from rest_framework import serializers +from .models import CustomUser, Book, Track, Note + +class BookSerializer(serializers.HyperlinkedModelSerializer): + + class Meta: + model = Book + fields = ('url', 'id', 'title', 'author', 'publication_date', 'genre', 'featured',) + + +class TrackSerializer(serializers.HyperlinkedModelSerializer): + user = serializers.SlugRelatedField(slug_field="username", read_only=True) + book = serializers.SlugRelatedField(slug_field="title", read_only=True) + + class Meta: + model = Track + fields = ('url', 'id', 'user', 'book', 'status',) + + +class NoteSerializer(serializers.HyperlinkedModelSerializer): + user = serializers.SlugRelatedField(slug_field="username", read_only=True) + book = serializers.SlugRelatedField(slug_field="title", read_only=True) + + class Meta: + model = Note + fields = ('url', 'id', 'user', 'book', 'created_at', 'note', 'private', 'page',) diff --git a/library_api/tests.py b/library_api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/library_api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/library_api/urls.py b/library_api/urls.py new file mode 100644 index 0000000..dab49e4 --- /dev/null +++ b/library_api/urls.py @@ -0,0 +1,20 @@ +from django.urls import path +from rest_framework.urlpatterns import format_suffix_patterns +from library_api import views + +urlpatterns = [ + path('books/', views.BookList.as_view(), name="book-list"), + path('books//', views.BookDetail.as_view(), name="book-detail"), + path('books/featured', views.FeaturedBookList.as_view(), name="featured-list"), + path('tracks/', views.UserTrackList.as_view(), name="track-list"), + path('tracks//', views.TrackDetail.as_view(), name="track-detail"), + path('books//tracks/', views.BookTrackList.as_view(), name="book-track-list"), + path('books//tracks/', views.BookTrackDetail.as_view(), name="book-track-detail"), + path('notes/', views.UserNoteList.as_view(), name="note-list"), + path('notes//', views.NoteDetail.as_view(), name="note-detail"), + path('books//notes/', views.BookNoteList.as_view(), name="book-note-list"), + path('books//notes/', views.BookNoteDetail.as_view(), name="book-note-detail"), + path('', views.api_root), +] + +urlpatterns = format_suffix_patterns(urlpatterns) diff --git a/library_api/views.py b/library_api/views.py new file mode 100644 index 0000000..6708ffa --- /dev/null +++ b/library_api/views.py @@ -0,0 +1,127 @@ +from django.shortcuts import render, get_object_or_404, redirect +from django.contrib.auth.models import User +from rest_framework import generics, permissions, filters +from rest_framework.decorators import api_view, action +from rest_framework.response import Response +from rest_framework.reverse import reverse +from rest_framework.serializers import ValidationError +from .models import Book, Track, Note, CustomUser +from .serializers import BookSerializer, TrackSerializer, NoteSerializer +from .permissions import IsAdminOrReadOnly, IsOwnerOrReadOnly +from django.db import IntegrityError +from django_filters.rest_framework import DjangoFilterBackend + + +class BookList(generics.ListCreateAPIView): + search_fields = ['title', 'author'] + filter_backends = (filters.SearchFilter,) + queryset = Book.objects.all() + serializer_class = BookSerializer + permission_classes = (permissions.IsAuthenticated,) + + def perform_create(self, serializer): + try: + serializer.save() + except IntegrityError: + raise ValidationError({"error": "Title already exists for this author"}) + + +class BookDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Book.objects.all() + serializer_class = BookSerializer + permission_classes = (IsAdminOrReadOnly,) + + +class FeaturedBookList(generics.ListAPIView): + queryset = Book.objects.filter(featured=True) + serializer_class = BookSerializer + permission_classes = (permissions.IsAuthenticated,) + + +class UserTrackList(generics.ListAPIView): + queryset = Track.objects.all() + serializer_class = TrackSerializer + permission_classes = (IsOwnerOrReadOnly,) + filter_backends = [DjangoFilterBackend] + filterset_fields = ['status'] + + def get_queryset(self): + queryset = self.request.user.tracks.all() + return queryset.order_by('book') + + +class TrackDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Track.objects.all() + serializer_class = TrackSerializer + permission_classes = (IsOwnerOrReadOnly,) + + +class BookTrackList(generics.ListCreateAPIView): + queryset = Track.objects.all() + serializer_class = TrackSerializer + permission_classes = (permissions.IsAuthenticated,) + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.filter(book=self.kwargs['book_pk']).order_by('book') + + def perform_create(self, serializer): + book = get_object_or_404(Book, pk=self.kwargs['book_pk']) + try: + serializer.save(user=self.request.user, book=book) + except IntegrityError: + raise ValidationError({"error": "You've already recorded a status for this book"}) + + +class BookTrackDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Track.objects.all() + serializer_class = TrackSerializer + permission_classes = (IsOwnerOrReadOnly,) + + +class UserNoteList(generics.ListAPIView): + queryset = Note.objects.all() + serializer_class = NoteSerializer + permission_classes = (IsOwnerOrReadOnly,) + filter_backends = [DjangoFilterBackend] + filterset_fields = ['private'] + + def get_queryset(self): + queryset = self.request.user.notes.all() + return queryset.order_by('-created_at') + + +class NoteDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Note.objects.all() + serializer_class = NoteSerializer + permission_classes = (IsOwnerOrReadOnly,) + + +class BookNoteList(generics.ListCreateAPIView): + queryset = Note.objects.all() + serializer_class = NoteSerializer + permission_classes = (permissions.IsAuthenticated,) + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.filter(book=self.kwargs['book_pk'], private=False).order_by('-created_at') + + def perform_create(self, serializer): + book = get_object_or_404(Book, pk=self.kwargs['book_pk']) + serializer.save(user=self.request.user, book=book) + + +class BookNoteDetail(generics.RetrieveUpdateDestroyAPIView): + queryset = Note.objects.all() + serializer_class = NoteSerializer + permission_classes = (IsOwnerOrReadOnly,) + + +@api_view(['GET']) +def api_root(request, format=None): + return Response({ + 'books': reverse('book-list', request=request, format=format), + 'tracks': reverse('track-list', request=request, format=format), + 'notes': reverse('note-list', request=request, format=format), + 'featured': reverse('featured-list', request=request, format=format), + }) diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..8e7ac79 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.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() diff --git a/static/css/styles.css b/static/css/styles.css new file mode 100644 index 0000000..e69de29