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 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 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 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() @process_method('GET', 404)
def add_book(request):
    """
    Returns a page for adding book.
    """
    if request.user.is_authenticated():
        categories = Category.objects.all().order_by('category_name')
        languages = Language.objects.all()

        return render(request, 'add_book.html', {'categories': categories, 'languages': languages})
    else:
        return redirect('index') @process_ajax(404)
@process_form('GET', GenerateAuthorsForm, 404)
def generate_authors(request, form):
    """
    Returns a list of authors.
    """
    list_of_authors = Author.get_authors_list(form.cleaned_data['part'], True)

    return HttpResponse(json.dumps(list_of_authors), content_type='application/json') @process_ajax(404)
@process_form('GET', GenerateBooksForm, 404)
def generate_books(request, form):
    """
    Returns a list of books.
    """
    list_of_books = Book.generate_existing_books(form.cleaned_data['part'])

    return HttpResponse(json.dumps(list_of_books), content_type='application/json') @process_method('POST', 404)
@process_form('POST', AddBookForm, 404)
def add_book_successful(request, form):
    """
    Creates new book object.
    """
    with transaction.atomic():
        related_data = Book.get_related_objects_for_create(request.user.id, form)

        book = Book.objects.create(
            book_name=form.cleaned_data['bookname'],
            id_author=related_data.author,
            id_category=related_data.category,
            description=form.cleaned_data['about'],
            language=related_data.lang,
            book_file=form.cleaned_data['bookfile'],
            who_added=related_data.user,
            private_book=form.cleaned_data['private']
        )
        AddedBook.objects.create(id_user=related_data.user, id_book=book)

        logger.info("User '{}' uploaded book with id: '{}' and name: '{}' on category: '{}'."
                    .format(related_data.user, book.id, book.book_name, related_data.category))
        os.chmod(book.book_file.path, READ_PRIVILEGES)

        return HttpResponse(reverse('book', kwargs={'book_id': book.id}), status=200)
import json
import os
from django.contrib.auth.models import User
from django.core.files.uploadedfile import SimpleUploadedFile
from django.test import TestCase, Client, mock
from django.shortcuts import reverse
from ...models import Category, Language, Author, TheUser, Book, AddedBook
from ...views.add_book_views import add_book, generate_authors, generate_books, add_book_successful

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

    # ------------------------------------------------------------------------------------------------------------------
    def test_generate_authors_success_parts(self):
        response = self.logged_client.get(reverse('generate_authors'), {'part': 'the'}, HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 1)
        self.assertEqual(response_content, ['The new author'])

        response = self.logged_client.get(reverse('generate_authors'), {'part': 'new'}, HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 2)
        self.assertEqual(response_content, ['New Author Name', 'The new author'])

        response = self.logged_client.get(reverse('generate_authors'), {'part': 'e'}, HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 4)
        self.assertEqual(
            response_content,
            ['New Author Name', 'A best one', 'The new author', '&lt;AuthorSpecialSymbols&gt;&amp;&quot;']
        )

    # ------------------------------------------------------------------------------------------------------------------
    def test_generate_authors_success_full_name(self):
        response = self.logged_client.get(reverse('generate_authors'), {'part': 'New Author Name'},
                                          HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 1)
        self.assertEqual(response_content, ['New Author Name'])

        response = self.logged_client.get(reverse('generate_authors'), {'part': 'A best one'},
                                          HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 1)
        self.assertEqual(response_content, ['A best one'])

    # ------------------------------------------------------------------------------------------------------------------
    def test_generate_authors_success_different_case(self):
        response = self.logged_client.get(reverse('generate_authors'), {'part': 'AUTHOR'},
                                          HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 3)
        self.assertEqual(
            response_content, ['New Author Name', 'The new author', '&lt;AuthorSpecialSymbols&gt;&amp;&quot;']
        )

        response = self.logged_client.get(reverse('generate_authors'), {'part': 'author'},
                                          HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 3)
        self.assertEqual(
            response_content, ['New Author Name', 'The new author', '&lt;AuthorSpecialSymbols&gt;&amp;&quot;']
        )

        response = self.logged_client.get(reverse('generate_authors'), {'part': 'aUthOR'},
                                          HTTP_X_REQUESTED_WITH=self.xhr)
        response_content = json.loads(response.content.decode('utf-8'))

        self.assertEqual(response.resolver_match.func, generate_authors)
        self.assertEqual(response.status_code, 200)
        self.assertTrue(isinstance(response_content, list))
        self.assertEqual(len(response_content), 3)
        self.assertEqual(
            response_content, ['New Author Name', 'The new author', '&lt;AuthorSpecialSymbols&gt;&amp;&quot;']
        )

    # ------------------------------------------------------------------------------------------------------------------
    def test_generate_books_not_ajax(self):
        response = self.logged_client.get(reverse('generate_books'))

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

    # ------------------------------------------------------------------------------------------------------------------
    def test_generate_books_missing_params(self):
        response = self.logged_client.get(reverse('generate_books'), {}, HTTP_X_REQUESTED_WITH=self.xhr)

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

    # ------------------------------------------------------------------------------------------------------------------
    def test_generate_books_invalid_params(self):
        response = self.logged_client.get(reverse('generate_books'), {'part': ''}, HTTP_X_REQUESTED_WITH=self.xhr)

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

        response = self.logged_client.get(reverse('generate_books'), {'part': 'y' * 170},
                                          HTTP_X_REQUESTED_WITH=self.xhr)


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