Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
/env
/.idea
/env*
/.*
/coverage
*.pyc
*.coverage
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Books project

One Paragraph of project description goes here - TODO

## Getting Started

### Prerequisites
Ensure that you have installed the 3rd python version.

### Installing

- Run `virtualenv --python=python3 env`. Please name your environment starting with env.
- Enter created environment with running `env\Scripts\activate` on Windows or `source env/bin/activate` on Mac OS.
- Install all necessary requirements `pip install -r requirements.txt`

## Running project

Ensure that you are using your virtual environment.

Run
```
python manage.py runserver
```
to start server work.

## Tests

Run `pytest` command.
You will receive general testing status in console. Command will create coverage folder with coverage of all modules.
File renamed without changes.
4 changes: 2 additions & 2 deletions books/books/asgi.py → app/asgi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
ASGI config for books project.
ASGI config for app project.

It exposes the ASGI callable as a module-level variable named ``application``.

Expand All @@ -11,6 +11,6 @@

from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'books.settings')
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings')

application = get_asgi_application()
34 changes: 34 additions & 0 deletions app/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from rest_framework import authentication
from rest_framework import exceptions
import jwt

key = 'secret'


class UserAuthentication(authentication.BaseAuthentication):

def authenticate(self, request):
auth = authentication.get_authorization_header(request).split()

if not auth or auth[0].lower() != b'bearer' or len(auth) == 1:
raise exceptions.AuthenticationFailed('Invalid token header. No credentials provided.')
elif len(auth) > 2:
raise exceptions.AuthenticationFailed('Invalid token header. Token string should not contain spaces.')

try:
token = auth[1].decode()
decoded_token = jwt.decode(token, key, algorithm='HS256')
request.META['HTTP_CUSTOM_HEADER'] = decoded_token['books_ids']
except jwt.ExpiredSignatureError:
raise exceptions.AuthenticationFailed('Token was expired')
except (jwt.DecodeError, UnicodeError):
raise exceptions.AuthenticationFailed('Invalid token header.')

return decoded_token, None


class UsersViewAuthentication(UserAuthentication):

def authenticate(self, request):
if request.method == 'GET':
return super().authenticate(request)
File renamed without changes.
24 changes: 24 additions & 0 deletions app/books/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.0.1 on 2020-01-13 13:05

import datetime
from django.db import migrations, models


class Migration(migrations.Migration):

initial = True

dependencies = [
]

operations = [
migrations.CreateModel(
name='Book',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=50)),
('description', models.TextField()),
('creation_date', models.DateTimeField(default=datetime.datetime.now)),
],
),
]
18 changes: 18 additions & 0 deletions app/books/migrations/0002_auto_20200113_1507.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 3.0.1 on 2020-01-13 13:07

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('books', '0001_initial'),
]

operations = [
migrations.AlterField(
model_name='book',
name='description',
field=models.TextField(blank=True),
),
]
20 changes: 20 additions & 0 deletions app/books/migrations/0003_book_creator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.0.1 on 2020-01-20 13:16

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('users', '0001_initial'),
('books', '0002_auto_20200113_1507'),
]

operations = [
migrations.AddField(
model_name='book',
name='creator',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='users.User'),
),
]
20 changes: 20 additions & 0 deletions app/books/migrations/0004_auto_20200120_1518.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.0.1 on 2020-01-20 13:18

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('users', '0001_initial'),
('books', '0003_book_creator'),
]

operations = [
migrations.AlterField(
model_name='book',
name='creator',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.User'),
),
]
File renamed without changes.
16 changes: 16 additions & 0 deletions app/books/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from django.db import models
from datetime import datetime
from app.users.models import User


class Book(models.Model):
name = models.CharField(max_length=50)
description = models.TextField(blank=True)
creation_date = models.DateTimeField(default=datetime.now)
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books')

def obj(self):
return {'id': self.id, 'name': self.name, 'description': self.description, 'creator': self.creator.obj()}

def __str__(self):
return 'id: ' + str(self.id) + ', name:' + str(self.name) + ', description:' + str(self.description)
13 changes: 13 additions & 0 deletions app/books/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from rest_framework.exceptions import AuthenticationFailed


def book_write_permission(f):

def check_permission(self, request, book_id):
if int(book_id) in request.META['HTTP_CUSTOM_HEADER']:
return f(self, request, book_id)
else:
raise AuthenticationFailed()

return check_permission

17 changes: 17 additions & 0 deletions app/books/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.conf.urls import url
from . import views

urlpatterns = [
url('^$', views.BooksView.as_view(
{
'get': 'get',
'post': 'post'
}
), name='books_list'),
url('^(?P<book_id>[0-9]+)/?$', views.SingleBookView.as_view(
{
'get': 'get',
'delete': 'delete',
'put': 'update'
}), name='book'),
]
63 changes: 63 additions & 0 deletions app/books/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from django.http import JsonResponse
from rest_framework.viewsets import ViewSet
from .models import Book
from django.core.exceptions import ObjectDoesNotExist
from app.auth import UserAuthentication
from .permissions import book_write_permission


class BooksView(ViewSet):
authentication_classes = (UserAuthentication,)

def get(self, request):
all_books = list(Book.objects.all())
return JsonResponse({'books': [b.obj() for b in all_books]})

def post(self, request):
name = request.data.get('name')
description = request.data.get('description')
creator_id = request.data.get('creator')
if name and creator_id:
new_book = Book.objects.create(name=name, description=description, creator_id=creator_id)
return JsonResponse({'message': 'Successfully created book, id: ' + str(new_book.id)}, status=201)
return JsonResponse({'message': 'Invalid data'}, status=400)


class SingleBookView(ViewSet):
authentication_classes = (UserAuthentication,)

def get(self, request, book_id):
try:
book = Book.objects.get(id=book_id)
return JsonResponse({'book': book.obj()})
except ObjectDoesNotExist:
return JsonResponse({'message': 'Book doesn\'t exist'}, status=401)

@book_write_permission
def delete(self, request, book_id):
try:
removed_book = Book.objects.get(id=book_id)
Book.objects.filter(id=book_id).delete()
response_message = {'message': 'removed book'}
response_message['book'] = removed_book.obj()
return JsonResponse(response_message)
except ObjectDoesNotExist:
return JsonResponse({'message': 'Book doesn\'t exist'}, status=401)

@book_write_permission
def update(self, request, book_id):
name = request.data.get('name')
description = request.data.get('description')
if name or description:
try:
existing_book = Book.objects.filter(id=book_id)
if not existing_book:
return JsonResponse({'message': 'Book doesn\'t exist'}, status=401)
if name:
existing_book.update(name=name)
if description:
existing_book.update(description=description)
return JsonResponse({'message': 'Successfully updated book', 'book': existing_book[0].obj()})
except ObjectDoesNotExist:
return JsonResponse({'message': 'Book doesn\'t exist'}, status=401)
return JsonResponse({'message': 'Invalid data'}, status=400)
13 changes: 8 additions & 5 deletions books/books/settings.py → app/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Django settings for books project.
Django settings for app project.

Generated by 'django-admin startproject' using Django 3.0.1.

Expand Down Expand Up @@ -31,25 +31,28 @@
# Application definition

INSTALLED_APPS = [
'app.books',
'app.users',
'rest_framework.authtoken',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.staticfiles'
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'books.urls'
ROOT_URLCONF = 'app.urls'

TEMPLATES = [
{
Expand All @@ -67,7 +70,7 @@
},
]

WSGI_APPLICATION = 'books.wsgi.application'
WSGI_APPLICATION = 'app.wsgi.application'


# Database
Expand Down
11 changes: 9 additions & 2 deletions books/books/urls.py → app/urls.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""books URL Configuration
"""app URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/3.0/topics/http/urls/
Expand All @@ -15,8 +15,15 @@
"""
from django.contrib import admin
from django.urls import include, path
from django.conf.urls import url
from app.users.views import UserLogin

urlpatterns = [
path('polls/', include('polls.urls')),
path('users/', include('app.users.urls')),
path('books/', include('app.books.urls')),
path('admin/', admin.site.urls),
url('token/', UserLogin.as_view(
{
'post': 'post'
}), name='login')
]
File renamed without changes.
22 changes: 22 additions & 0 deletions app/users/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 3.0.1 on 2020-01-20 12:56

from django.db import migrations, models


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')),
('username', models.CharField(max_length=30)),
('password_hash', models.TextField(max_length=64)),
],
),
]
Empty file.
Loading