class ReportForm(forms.Form):
    text = forms.CharField(
        widget=forms.Textarea(
            attrs={
                'class': 'input input-colors',
                'id': 'report-text',
                'placeholder': 'Название, автор, категория, все что угодно...'}
        ),
        max_length=5000
    ) class Author(models.Model):
    """
    Class for author objects in database.
    """
    author_name = models.CharField(max_length=100)

    # ------------------------------------------------------------------------------------------------------------------
    def __str__(self):
        return self.author_name

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def get_authors_list(author_part, do_escape=False):
        """
        Returns a list of authors.

        :param str  author_part: The part of author name.
        :param bool do_escape:   Defines if special symbols of author names must be escaped.

        :return list[str]:
        """
        authors = Author.objects.filter(author_name__icontains=author_part)[:10].values_list('author_name', flat=True)
        return [escape(author) for author in authors] if do_escape else list(authors) class Book(models.Model):
    """
    Class for book objects in database.
    """
    book_name = models.CharField(max_length=150)
    id_author = models.ForeignKey(Author)
    id_category = models.ForeignKey(Category)
    description = models.CharField(max_length=1000, blank=True)
    language = models.ForeignKey(Language)
    photo = models.ImageField(blank=True, upload_to='book_cover')
    book_file = models.FileField(upload_to='book_file')
    who_added = models.ForeignKey(TheUser)
    upload_date = models.DateTimeField(auto_now=True)
    private_book = models.BooleanField(default=False)
    blocked_book = models.BooleanField(default=False)

    # ------------------------------------------------------------------------------------------------------------------
    def __str__(self):
        return "{0}, {1}, язык({2})".format(self.book_name, self.id_author, self.language)

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def get_related_objects_for_create(user_id, book_form):
        """
        Selects related objects to book instance when create new book; creates author object if needed.

        :param int user_id: The id of user.
        :param app.forms.AddBookForm book_form: The form with received data.

        :return: A dict of objects related to book.
        """
        try:
            author = Author.objects.get(author_name__iexact=book_form.cleaned_data['author'])
        except ObjectDoesNotExist:
            author = Author.objects.create(author_name=book_form.cleaned_data['author'])

            logger.info("Created new author with name: '{}' and id: '{}'."
                        .format(author.author_name, author.id))

        return BookRelatedData(
            author,
            Category.objects.get(category_name=book_form.cleaned_data['category']),
            Language.objects.get(language=book_form.cleaned_data['language']),
            TheUser.objects.get(id_user=user_id)
        )

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def get_related_objects_create_api(user, data):
        """
        Selects related object for book instance when create a new book; creates author object if needed.
        """
        try:
            author = Author.objects.get(author_name__iexact=data.get('author'))
        except ObjectDoesNotExist:
            author = Author.objects.create(author_name=data.get('author'))

            logger.info("Created new author with name: '{}' and id: '{}'."
                        .format(author.author_name, author.id))

        return BookRelatedData(
            author,
            Category.objects.get(category_name=data.get('category')),
            Language.objects.get(language=data.get('language')),
            None
        )

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def get_related_objects_selected_book(user, book_id, user_key=''):
        """
        Returns the related objects of selected book

        :param django.db.models.User user: The request for selecting book.
        :param int book_id:                The ID of selected book.
        :param str user_key:               The key which is used to get user if it's an API call.

        :return: Related objects.
        """
        book = Book.objects.get(id=book_id)
        book_rating = BookRating.objects.filter(id_book=book)

        try:
            if not user.is_anonymous:
                added_book = AddedBook.objects.get(id_user=TheUser.objects.get(id_user=user), id_book=book)
            else:
                the_user = TheUser.objects.get(auth_token=user_key)
                added_book = AddedBook.objects.get(id_user=the_user, id_book=book)

        except ObjectDoesNotExist:
            added_book = None

        comments = BookComment.objects.filter(id_book=Book.objects.get(id=book_id)).order_by('-id')

        return {'book': book,
                'avg_book_rating': book_rating.aggregate(Avg('rating')),
                'book_rating_count': book_rating.count(),
                'added_book': added_book,
                'comments': comments}

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def sort_by_book_name(user, category):
        """
        Sorts books by book name.

        :param django.contrib.auth.models.User  user:     The request user.
        :param app.models.Category              category: The category.

        :return list[dict[str, str]]: list of books with data.
        """
        books = Book.objects.filter(id_category=category).order_by('book_name')
        filtered_books = Book.exclude_private_books(user, books)

        return Book.generate_books(filtered_books)

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def sort_by_author(user, category):
        """
        Sorts books by author.

        :param django.contrib.auth.models.User  user:     The request user.
        :param app.models.Category              category: The category.

        :return list[dict[str, str]]: list of books with data.
        """
        books = Book.objects.filter(id_category=category).order_by('id_author__author_name')
        filtered_books = Book.exclude_private_books(user, books)

        return Book.generate_books(filtered_books)

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def sort_by_estimation(user, category):
        """
        Sorts books by average count of estimation of each book. Uses aggregate function.

        :param django.contrib.auth.models.User  user:     The request user.
        :param app.models.Category              category: The category.

        :return: The list with sorted books.
        """
        books = []
        filtered_books = Book.exclude_private_books(user, Book.objects.filter(id_category=category))

        for item in filtered_books:
            book_rating = BookRating.objects.filter(id_book=item).aggregate(Avg('rating'))
            book = {
                'id': item.id,
                'name': escape(item.book_name),
                'author': escape(item.id_author.author_name),
                'url': item.photo.url if item.photo else '',
                'rating': book_rating['rating__avg']
            }
            books.append(book)

        return sorted(books, key=lambda info: (info['rating'] is not None, info['rating']), reverse=True)

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def sort_by_readable(user, category=None, count=9):
        """
        Sorts books by most readable criterion. Uses aggregate 'count' function.

        :param django.contrib.auth.models.User  user:     The request user.
        :param app.models.Category              category: The category.
        :param int                              count:    The count of books which must be returned.

        :return: The list with sorted books.
        """
        if category:
            filtered_books = Book.exclude_private_books(user, Book.objects.filter(id_category=category))
            added_books = AddedBook.objects.filter(id_book__id_category=category).values('id_book')
        else:
            filtered_books = Book.exclude_private_books(user, Book.objects.all())
            added_books = AddedBook.objects.values('id_book')

        annotations = added_books.annotate(read_count=Count('id_book')).order_by('-read_count')[:count]

        filtered_books = filtered_books.filter(
            id__in=[annotation['id_book'] for annotation in annotations]
        )

        sorted_books = []
        for annotation in annotations:
            compared_book = filtered_books.filter(id=annotation['id_book'])

            if compared_book:
                sorted_books.append(filtered_books.get(id=compared_book[0].id))

        generated_books = [
            {
                'id': item.id,
                'name': escape(item.book_name),
                'author': escape(item.id_author.author_name),
                'url': item.photo.url if item.photo else ''
            }
            for item in sorted_books
        ]

        return generated_books

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def generate_books(filtered_books):
        """
        Generates list with books for specific data and special criterion.

        :param list filtered_books: The list of books after fetching them from database.
        :return list[dict[str, str]]: list of books with data.
        """
        books = [
            {
                'id': item.id,
                'name': escape(item.book_name),
                'author': escape(item.id_author.author_name),
                'url': item.photo.url if item.photo else '',
                'upload_date': item.upload_date.strftime('%d-%m-%Y')
            }
            for item in filtered_books
        ]

        return books

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def fetch_books(search_data):
        """
        Generates list of books, fetched by different criterion depending on 'search_data' argument.

        :param str search_data: The string with data by which create search books.
        :return: The generated list of books.
        """
        filtered_books = list(Book.objects.filter(book_name=search_data))
        filtered_books += list(Book.objects.filter(book_name__icontains=search_data))
        filtered_books += list(Book.objects.filter(id_author__author_name=search_data))
        filtered_books += list(Book.objects.filter(id_author__author_name__icontains=search_data))

        return list(set(filtered_books))

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def generate_existing_books(book_part):
        """
        Returns a list of books.

        :param str book_part: Criterion for returning books.
        :return:
        """
        return [{'url': reverse('book', args=[escape(item[0])]), 'name': escape(item[1])} for item in
                Book.objects.filter(book_name__icontains=book_part,
                                    private_book=False)[:10].values_list('id', 'book_name')]

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def exclude_private_books(user, books):
        """
        Returns the list of books without private books which not depend to current user.

        :param django.contrib.auth.models.User                                  user:  The request user.
        :param django.db.models.query.QuerySet[.models.Book]/list[.models.Book] books: The given list of books.

        :return django.db.models.query.QuerySet[.models.Book]/list[.models.Book]: List of books.
        """
        if isinstance(books, list):
            filtered_books = [book for book in books if not book.private_book or book.who_added.id_user == user]
        else:
            if not user.is_anonymous:
                filtered_books = books.filter(Q(private_book=False) | Q(who_added__id_user=user))
            else:
                filtered_books = books.filter(Q(private_book=False))

        return filtered_books class AddedBook(models.Model):
    """
    Class for added book objects in database.
    """
    id_user = models.ForeignKey(TheUser)
    id_book = models.ForeignKey(Book)
    last_page = models.PositiveIntegerField(default=1)
    last_read = models.DateTimeField(auto_now=True)

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def get_user_added_books(user):
        """
        Returns the list of books which user added in his home library

        :param django.db.models.User user: The user instance.

        :return list[app.models.Book]:
        """
        if user.is_anonymous:
            return []

        return AddedBook.objects.filter(id_user=TheUser.objects.get(id_user=user)).order_by('-last_read')

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def get_count_added(book_id):
        """
        Returns the added count of selected book.

        :param int book_id: The id of book which we are counting.

        :return int: The count of added books
        """
        return AddedBook.objects.filter(id_book__id=book_id).count() class Category(models.Model):
    """
    Class for category objects in database.
    """
    category_name = models.CharField(max_length=30)

    # ------------------------------------------------------------------------------------------------------------------
    def __str__(self):
        return self.category_name class Language(models.Model):
    """
    Class for language objects in database.
    """
    language = models.CharField(max_length=30)

    # ------------------------------------------------------------------------------------------------------------------
    def __str__(self):
        return self.language class TheUser(models.Model):
    """
    Class for user objects in database.
    """
    REMINDER_TEMPLATE = json.dumps({
        "common": {
            "fb_page": True,
            "fb_group": True,
            "twitter": True,
            "vk": True,
            "disabled_all": False
        },
        "api": {
            "app_rate": True
        },
        "web": {
            "app_download": True,
        },
    })

    id_user = models.OneToOneField(User)
    user_photo = models.ImageField(blank=True, upload_to='user', storage=OverwriteStorage())
    auth_token = models.CharField(max_length=50, null=True, blank=True)
    subscription = models.BooleanField(default=True)
    reminder = models.CharField(max_length=256, null=False, default=REMINDER_TEMPLATE)

    # ------------------------------------------------------------------------------------------------------------------
    def __str__(self):
        return str(self.id_user)

    # ------------------------------------------------------------------------------------------------------------------
    def get_api_reminders(self):
        """
        Returns the reminders only necessary for API endpoints.
        """
        data = json.loads(self.reminder)

        mobile_data = dict(data['common'])
        mobile_data.update(data['api'])

        return mobile_data

    # ------------------------------------------------------------------------------------------------------------------
    def get_web_reminders(self):
        """
        Returns the reminders only necessary for web part.
        """
        data = json.loads(self.reminder)
        
        web_data = dict(data['common'])
        web_data.update(data['web'])

        return web_data

    # ------------------------------------------------------------------------------------------------------------------
    def update_reminder(self, field, value):
        """
        Updates the reminder status.
        """
        data = json.loads(self.reminder)

        for key in data:
            if field in data[key]:
                data[key][field] = value

        self.reminder = json.dumps(data)
        self.save() class BookRating(models.Model):
    """
    Class for book rating objects in database.
    """
    id_user = models.ForeignKey(TheUser)
    id_book = models.ForeignKey(Book)
    rating = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(10)]) class BookComment(models.Model):
    """
    Class for book comment objects in database.
    """
    id_user = models.ForeignKey(TheUser)
    id_book = models.ForeignKey(Book)
    text = models.CharField(max_length=500)
    posted_date = models.DateField(auto_now=True) @process_method('GET', 404)
def all_categories(request):
    """
    Returns page with book categories.
    """
    categories = Category.objects.all().order_by('category_name')
    most_readable_books = Book.sort_by_readable(request.user, count=MOST_READ_BOOKS_COUNT)
    books_count = Book.objects.all().count()

    return render(request, 'categories.html', {'categories': categories,
                                               'most_readable_books': most_readable_books[:9],
                                               'books_count': books_count}) @process_method('GET', 404)
def selected_category(request, category_id):
    """
    Returns page with selected category.
    """
    category = get_object_or_404(Category, id=category_id)
    books = Book.objects.filter(id_category=category).order_by('book_name')
    filtered_books = Book.exclude_private_books(request.user, books)

    paginator = Paginator(filtered_books, settings.BOOKS_PER_PAGE)
    page = paginator.page(1)

    context = {
        'category': category,
        'books': page.object_list,
        'total_books_count': books.count(),
        'has_next': page.has_next()
    }
    return render(request, 'selected_category.html', context) @process_method('GET', 404)
def selected_author(request, author_id):
    """
    Returns a page with a list of books of a selected author.
    """
    author = get_object_or_404(Author, id=author_id)
    books = Book.objects.filter(id_author=author).order_by('book_name')

    filtered_books = Book.exclude_private_books(request.user, books)

    context = {'author': author, 'books': filtered_books}
    return render(request, 'selected_author.html', context) @process_ajax(404)
@process_form('GET', SortForm, 400)
def sort(request, form):
    """
    Returns data sorted data depending on criterion.
    """
    criterion_dict = {'book_name': Book.sort_by_book_name,
                      'author': Book.sort_by_author,
                      'estimation': Book.sort_by_estimation}

    category = Category.objects.get(id=form.cleaned_data['category'])
    books_count = Book.objects.filter(id_category=category).count()

    if form.cleaned_data['criterion'] == 'most_readable':
        books = Book.sort_by_readable(request.user, category, books_count)
    else:
        books = criterion_dict[form.cleaned_data['criterion']](request.user, category)

    paginator = Paginator(books, settings.BOOKS_PER_PAGE)
    page = paginator.page(form.cleaned_data['page'])

    context = {
        'category': category.id,
        'criterion': form.cleaned_data['criterion'],
        'books': page.object_list,
        'has_next': page.has_next(),
        'next_page': page.next_page_number() if page.has_next() else paginator.num_pages
    }

    return HttpResponse(json.dumps(context), content_type='application/json') @process_ajax(404)
@process_form('GET', SearchBookForm, 400)
def find_books(request, form):
    """
    Generates list with books of data which user entered. At first it check full equality in name,
    after tries to check if contains some part of entered data.
    """
    search_data = form.cleaned_data['data']
    filtered_books = Book.exclude_private_books(request.user, Book.fetch_books(search_data))

    paginator = Paginator(filtered_books, settings.BOOKS_PER_PAGE)
    page = paginator.page(form.cleaned_data['page'])

    response = {
        'books': Book.generate_books(page.object_list),
        'has_next': page.has_next(),
        'next_page': page.next_page_number() if page.has_next() else paginator.num_pages
    }
    return HttpResponse(json.dumps(response), content_type='application/json') @process_ajax(404)
@process_form('GET', BookPagingForm, 400)
def load_books(request, category_id, form):
    """
    Ajax request handler for outputting books page by page.
    """
    category = get_object_or_404(Category, id=category_id)
    books = Book.objects.filter(id_category=category).order_by('book_name')
    filtered_books = Book.exclude_private_books(request.user, books)

    paginator = Paginator(filtered_books, settings.BOOKS_PER_PAGE)
    page = paginator.page(form.cleaned_data['page'])

    response = {
        'category_id': category_id,
        'books': Book.generate_books(page.object_list),
        'has_next': page.has_next(),
        'next_page': page.next_page_number() if page.has_next() else paginator.num_pages
    }
    return HttpResponse(json.dumps(response), content_type='application/json') def selected_book(request, book_id):
    """
    Returns a page with selected book.
    """
    get_object_or_404(Book, id=book_id)
    rel_objects = Book.get_related_objects_selected_book(request.user, book_id)

    user_rated = None

    if not request.user.is_anonymous:
        user = TheUser.objects.get(id_user=request.user)
        user_rated = BookRating.objects.filter(id_book__id=book_id, id_user=user)
    else:
        user = request.user

    if rel_objects['book'].private_book and rel_objects['book'].who_added != user:
        return HttpResponse(status=404)

    recommend_books = get_recommend(
        request.user, AddedBook.get_user_added_books(request.user), RANDOM_BOOKS_COUNT, [book_id]
    )
    book_rating = rel_objects['avg_book_rating']['rating__avg']
    book_rating_count = rel_objects['book_rating_count']

    comments = rel_objects['comments']
    comments_paginator = Paginator(comments, COMMENTS_PER_PAGE)
    page = comments_paginator.page(COMMENTS_START_PAGE)

    context = {
        'book': rel_objects['book'],
        'added_book': rel_objects['added_book'],
        'added_book_count': AddedBook.get_count_added(book_id),
        'comments': page.object_list,
        'comments_page': COMMENTS_START_PAGE,
        'comments_has_next_page': page.has_next(),
        'book_rating': book_rating if book_rating else '-',
        'book_rating_count': '({})'.format(book_rating_count) if book_rating_count else '',
        'estimation_count': range(1, 11),
        'user': user,
        'recommend_books': recommend_books,
        'user_rated': user_rated[0] if user_rated else None,
        'report_form': ReportForm()
    }

    return render(request, 'selected_book.html', context) @process_ajax(404)
@process_form('POST', BookHomeForm, 400)
def add_book_to_home(request, form):
    """
    Adds book to list of user's added books.
    """
    user = TheUser.objects.get(id_user=request.user)
    book = Book.objects.get(id=form.cleaned_data['book'])

    if book.private_book and book.who_added != user:
        return HttpResponse(status=404)

    if book.blocked_book:
        return HttpResponse(status=400)

    if AddedBook.objects.filter(id_user=user, id_book=book).exists():
        return HttpResponse(status=404)

    AddedBook.objects.create(id_user=user, id_book=book)

    logger.info("User '{}' added book with id: '{}' to his own library."
                .format(request.user, form.cleaned_data['book']))

    return HttpResponse(json.dumps({'book_id': book.id}), content_type='application/json') @process_ajax(404)
@process_form('POST', BookHomeForm, 400)
def remove_book_from_home(request, form):
    """
    Removes book from list of user's added books.
    """
    book = get_object_or_404(Book, id=form.cleaned_data['book'])

    get_object_or_404(AddedBook, id_user=TheUser.objects.get(id_user=request.user), id_book=book).delete()
    logger.info("User '{}' removed book with id: '{}' from his own library."
                .format(request.user, form.cleaned_data['book']))

    if book.blocked_book:
        return HttpResponse(json.dumps(False), content_type='application/json')

    return HttpResponse(json.dumps(True), content_type='application/json') @process_ajax(404)
@process_form('POST', ChangeRatingForm, 400)
def change_rating(request, form):
    with transaction.atomic():
        set_rating(request, form)

        book_rating = BookRating.objects.filter(id_book=Book.objects.get(id=form.cleaned_data['book']))

        data = {'avg_rating': round(book_rating.aggregate(Avg('rating'))['rating__avg'], 1),
                'rating_count': '({})'.format(book_rating.count())}

        return HttpResponse(json.dumps(data), content_type='application/json') @process_ajax(404)
@process_form('POST', AddCommentForm, 400)
def add_comment(request, form):
    comment = BookComment.objects.create(id_user=TheUser.objects.get(id_user=request.user),
                                         id_book=Book.objects.get(id=form.cleaned_data['book']),
                                         text=form.cleaned_data['comment'])

    user = get_object_or_404(TheUser, id_user=request.user)
    user_photo = user.user_photo.url if user.user_photo else ''

    logger.info("User '{}' left comment with id: '{}' on book with id: '{}'."
                .format(user, comment.id, comment.id_book.id))

    response_data = {
        'username': escape(request.user.username),
        'user_photo': user_photo,
        'posted_date': comment.posted_date.strftime('%d-%m-%Y'),
        'text': escape(comment.text)
    }
    return HttpResponse(json.dumps(response_data), content_type='application/json') @process_ajax(404)
@process_form('POST', LoadCommentsForm, 400)
def load_comments(request, form):
    book = Book.objects.get(id=form.cleaned_data['book_id'])
    comments = BookComment.objects.filter(id_book=book).order_by('-id')
    next_page_num = form.cleaned_data['page'] + 1

    comments_paginator = Paginator(comments, COMMENTS_PER_PAGE)
    page = comments_paginator.page(next_page_num)

    json_comments = [{
        'username': escape(comment.id_user.id_user.username),
        'user_photo': comment.id_user.user_photo.url if comment.id_user.user_photo else '',
        'posted_date': comment.posted_date.strftime('%d-%m-%Y'),
        'text': escape(comment.text)
    } for comment in page.object_list]

    response_data = {
        'comments': json_comments,
        'current_page': next_page_num,
        'has_next_page': page.has_next(),
        'book_id': book.id
    }

    return HttpResponse(json.dumps(response_data), content_type='application/json') @process_method('POST', 400)
@process_form('POST', ReportForm, 400)
def report_book(request, form):
    """
    Creates a report of incorrect entered data.
    """
    SupportMessage.objects.create(
        email=request.user.email,
        text=form.cleaned_data['text']
    )
    return HttpResponse(status=200) class Utils:
    """
    Class with util functions which helps to generate some data.
    """

    # ------------------------------------------------------------------------------------------------------------------
    @staticmethod
    def generate_sort_dict(book):
        return {
            'id': book.id,
            'name': escape(book.book_name),
            'author': escape(book.id_author.author_name),
            'url': book.photo.url if book.photo else '',
            'upload_date': book.upload_date.strftime('%d-%m-%Y')
        }
import json
import os
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
from django.shortcuts import reverse
from django.test import TestCase, Client, override_settings
from ...forms import ReportForm
from ...models import Author, Book, AddedBook, Category, Language, TheUser, BookRating, BookComment
from ...views.library_views import all_categories, selected_category, selected_author, sort, find_books, load_books
from ...views.selected_book_views import (
    selected_book, add_book_to_home, remove_book_from_home, change_rating, add_comment, load_comments, report_book
)
from ..utils import Utils

                Utils.generate_sort_dict(self.book3),
                Utils.generate_sort_dict(self.book5)
            ],
            'has_next': False,
            'next_page': 2
        }

        self.assertEqual(response.resolver_match.func, find_books)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response_data, expected_response)

    # ------------------------------------------------------------------------------------------------------------------
    def test_load_books_not_ajax(self):
        response = self.anonymous_client.get(reverse('load_books', kwargs={'category_id': self.category.id}))

        self.assertEqual(response.resolver_match.func, load_books)
        self.assertEqual(response.status_code, 404)

    # ------------------------------------------------------------------------------------------------------------------
    def test_load_books_missing_page_param(self):
        response = self.anonymous_client.get(
            reverse('load_books', kwargs={'category_id': self.category.id}),
            {},
            HTTP_X_REQUESTED_WITH=self.xhr
        )

        self.assertEqual(response.resolver_match.func, load_books)
        self.assertEqual(response.status_code, 400)

    # ------------------------------------------------------------------------------------------------------------------
    def test_load_books_negative_page_param(self):
        response = self.anonymous_client.get(
            reverse('load_books', kwargs={'category_id': self.category.id}),
            {'page': -15},
            HTTP_X_REQUESTED_WITH=self.xhr
        )

        self.assertEqual(response.resolver_match.func, load_books)
        self.assertEqual(response.status_code, 400)

    # ------------------------------------------------------------------------------------------------------------------
    def test_load_books_success(self):
        response = self.anonymous_client.get(
            reverse('load_books', kwargs={'category_id': self.category.id}),
            {'page': 2},
            HTTP_X_REQUESTED_WITH=self.xhr
        )
        response_data = json.loads(response.content.decode('utf-8'))

        expected_books = [
            Utils.generate_sort_dict(self.book3),
            Utils.generate_sort_dict(self.book5)
        ]

        self.assertEqual(response.resolver_match.func, load_books)
        self.assertEqual(response.status_code, 200)
        self.assertIn('category_id', response_data)
        self.assertIn('books', response_data)
        self.assertIn('has_next', response_data)
        self.assertIn('next_page', response_data)
        self.assertEqual(response_data['category_id'], str(self.category.id))
        self.assertEqual(list(response_data['books']), expected_books)
        self.assertEqual(response_data['has_next'], False)
        self.assertEqual(response_data['next_page'], 2)

    # ------------------------------------------------------------------------------------------------------------------
    # Selected Book test cases.
    # Done here due to issues with Django / MySQL closed connection...

    def test_selected_book_not_existing_book(self):
        response = self.logged_client.get(
            reverse('book', kwargs={'book_id': 50000})
        )
        self.assertEqual(response.resolver_match.func, selected_book)
        self.assertEqual(response.status_code, 404)

    # ------------------------------------------------------------------------------------------------------------------
    def test_selected_book_is_private_for_anonymous_user(self):
        response = self.anonymous_client.get(
            reverse('book', kwargs={'book_id': self.book4.id})
        )
        self.assertEqual(response.resolver_match.func, selected_book)
        self.assertEqual(response.status_code, 404)

    # ------------------------------------------------------------------------------------------------------------------
    def test_selected_book_is_private_for_logged_not_added_user(self):
        response = self.logged_client2.get(
            reverse('book', kwargs={'book_id': self.book4.id})
        )
        self.assertEqual(response.resolver_match.func, selected_book)
        self.assertEqual(response.status_code, 404)

    # ------------------------------------------------------------------------------------------------------------------
    def test_selected_book_is_private_for_logged_who_added_user(self):
        response = self.logged_client.get(
            reverse('book', kwargs={'book_id': self.book4.id})
        )
        self.assertEqual(response.resolver_match.func, selected_book)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.context['book'], self.book4)
        self.assertIsNone(response.context['added_book'])
        self.assertEqual(response.context['added_book_count'], 0)
        self.assertEqual(len(response.context['comments']), 0)
        self.assertEqual(response.context['comments_page'], 1)
        self.assertFalse(response.context['comments_has_next_page'])
        self.assertEqual(response.context['book_rating'], '-')
        self.assertEqual(response.context['book_rating_count'], '')
        self.assertEqual(response.context['estimation_count'], range(1, 11))
        self.assertEqual(response.context['user'], self.the_user)
        self.assertEqual(len(response.context['recommend_books']), 0)
        self.assertIsNone(response.context['user_rated'])
        self.assertTrue(isinstance(response.context['report_form'], ReportForm))

    # ------------------------------------------------------------------------------------------------------------------
    def test_store_image(self):
        pass  # TODO add tests for storing images.

    # ------------------------------------------------------------------------------------------------------------------
    def test_add_book_to_home_not_ajax(self):
        response = self.logged_client.post(reverse('add_book_home_app'), {})

        self.assertEqual(response.resolver_match.func, add_book_to_home)
