diff --git a/README.md b/README.md index 33c0486..01348c0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ The application allows you to automate the process of generating a bibliography Supported citation styles: - ГОСТ Р 7.0.5-2008 +- APA ## Installation diff --git a/docs/source/index.rst b/docs/source/index.rst index f552b0d..30ff386 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,6 +7,7 @@ Поддерживаемые стили цитирования: - ГОСТ Р 7.0.5-2008 + - APA Установка ========= diff --git a/src/formatters/models.py b/src/formatters/models.py index c9236ca..78b8cc0 100644 --- a/src/formatters/models.py +++ b/src/formatters/models.py @@ -78,3 +78,50 @@ class ArticlesCollectionModel(BaseModel): publishing_house: str year: int = Field(..., gt=0) pages: str + +class DissertationModel(BaseModel): + """ + Модель диссертации: + .. code-block:: + DissertationModel( + author="Иванов И.М.", + title="Наука как искусство", + academic_degree="канд.", + science="экон.", + specialty_code="01.01.01", + publication_city="СПб.", + publication_year=2020, + page_count=199, + ) + """ + + author: str + title: str + academic_degree: str + science: str + specialty_code: str + publication_city: str + publication_year: int = Field(..., gt=0) + page_count: int = Field(..., gt=0) + + +class ArticleNewspaperModel(BaseModel): + """ + Модель статьи из газеты: + .. code-block:: + NewspaperArticleModel( + authors="Иванов И.М., Петров С.Н.", + article_title="Наука как искусство", + newspaper_name="Южный Урал", + publication_year=1980, + publication_date="01.10", + аrticle_number=5 + ) + """ + + authors: str + article_title: str + newspaper_name: str + publication_year: int = Field(..., gt=0) + publication_date: str + аrticle_number: int = Field(..., gt=0) diff --git a/src/formatters/styles/apa.py b/src/formatters/styles/apa.py new file mode 100644 index 0000000..04bcb52 --- /dev/null +++ b/src/formatters/styles/apa.py @@ -0,0 +1,103 @@ +""" +Стиль цитирования по American Psychological Association +""" +from string import Template + +from pydantic import BaseModel + +from formatters.models import BookModel, InternetResourceModel +from formatters.styles.base import BaseCitationStyle +from logger import get_logger + + +logger = get_logger(__name__) + + +class Book_APA(BaseCitationStyle): + """ + Форматирование Книг. + """ + + data: BookModel + + @property + def template(self) -> Template: + return Template("$authors ($year). $title $edition. $publishing_house.") + + def substitute(self) -> str: + + logger.info('Форматирование Книги "%s" ...', self.data.title) + + return self.template.substitute( + authors=self.data.authors, + title=self.data.title, + edition=self.get_edition(), + city=self.data.publication_city, + publishing_house=self.data.publishing_house, + year=self.data.publication_year, + pages=self.data.pages, + ) + + def get_edition(self) -> str: + """ + Получение информации об издательстве. + :return: Информация об издательстве. + """ + + return f"({self.data.edition} изд.)" if self.data.edition else "" + + +class InternetResource_APA(BaseCitationStyle): + """ + Форматирование Интернет-ресурсов. + """ + + data: InternetResourceModel + + @property + def template(self) -> Template: + return Template( + "$article. (n.d.). $website. Retrieved $access_date, from $link" + ) + + def substitute(self) -> str: + + logger.info('Форматирование Интернет-ресурса "%s" ...', self.data.article) + + return self.template.substitute( + article=self.data.article, + website=self.data.website, + link=self.data.link, + access_date=self.data.access_date, + ) + + +class Formatter_APA: + """ + Класс для форматирования списка источников по стандартам APA. + """ + + formatters_map = { + BookModel.__name__: Book_APA, + InternetResourceModel.__name__: InternetResource_APA, + } + + def __init__(self, models: list[BaseModel]) -> None: + """ + Конструктор для форматирования. + :param models: Список объектов для форматирования + """ + + formatted_items = [] + for model in models: + formatted_items.append(self.formatters_map.get(type(model).__name__(model))) + + self.formatted_items = formatted_items + + def format(self) -> list[BaseCitationStyle]: + """ + Форматирование списка источников. + :return: + """ + + return sorted(self.formatted_items, key=lambda item: item.formatted) \ No newline at end of file diff --git a/src/formatters/styles/gost.py b/src/formatters/styles/gost.py index b237f8a..bef0288 100644 --- a/src/formatters/styles/gost.py +++ b/src/formatters/styles/gost.py @@ -5,7 +5,7 @@ from pydantic import BaseModel -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, DissertationModel, ArticleNewspaperModel from formatters.styles.base import BaseCitationStyle from logger import get_logger @@ -34,9 +34,9 @@ def substitute(self) -> str: authors=self.data.authors, title=self.data.title, edition=self.get_edition(), - city=self.data.city, + city=self.data.publication_city, publishing_house=self.data.publishing_house, - year=self.data.year, + year=self.data.publication_year, pages=self.data.pages, ) @@ -96,12 +96,71 @@ def substitute(self) -> str: authors=self.data.authors, article_title=self.data.article_title, collection_title=self.data.collection_title, - city=self.data.city, + city=self.data.publication_city, publishing_house=self.data.publishing_house, - year=self.data.year, + year=self.data.publication_year, pages=self.data.pages, ) +class GOSTDissertationModel(BaseCitationStyle): + """ + Форматирование для Диссертации. + """ + + data: DissertationModel + + @property + def template(self) -> Template: + return Template( + "$author $title: дис. ... $academic_degree $science наук: $specialty_code $publication_city $publication_year. $page_count с." + ) + + def substitute(self) -> str: + + logger.info('Форматирование Диссертации "%s" ...', self.data.title) + + return self.template.substitute( + author=self.data.author, + title=self.data.title, + academic_degree=self.data.academic_degree, + science=self.data.science, + specialty_code=self.data.specialty_code, + publication_city=self.data.publication_city, + publication_year=self.data.publication_year, + page_count=self.data.page_count, + ) + + +class GOSTArticleNewspaper(BaseCitationStyle): + """ + Форматирование для Статьи из газеты. + """ + + data: ArticleNewspaperModel + + @property + def template(self) -> Template: + return Template( + "$authors $article_title // $newspaper_name. – $publication_year. - $publication_date. - №$аrticle_number." + ) + + def substitute(self) -> str: + + logger.info('Форматирование Статьи из газеты "%s" ...', self.data.article_title) + + return self.template.substitute( + authors=self.data.authors, + article_title=self.data.article_title, + newspaper_name=self.data.newspaper_name, + publication_year=self.data.publication_year, + publication_date=self.data.publication_date, + аrticle_number=self.data.аrticle_number, + ) + + + + + class GOSTCitationFormatter: """ @@ -112,6 +171,9 @@ class GOSTCitationFormatter: BookModel.__name__: GOSTBook, InternetResourceModel.__name__: GOSTInternetResource, ArticlesCollectionModel.__name__: GOSTCollectionArticle, + DissertationModel.__name__: GOSTDissertationModel, + ArticleNewspaperModel.__name__: GOSTArticleNewspaper, + } def __init__(self, models: list[BaseModel]) -> None: diff --git a/src/main.py b/src/main.py index 7a9fa8e..c986f60 100644 --- a/src/main.py +++ b/src/main.py @@ -10,6 +10,7 @@ from readers.reader import SourcesReader from renderer import Renderer from settings import INPUT_FILE_PATH, OUTPUT_FILE_PATH +from formatters.styles.apa import Formatter_APA logger = get_logger(__name__) @@ -77,9 +78,16 @@ def process_input( ) models = SourcesReader(path_input).read() - formatted_models = tuple( - str(item) for item in GOSTCitationFormatter(models).format() - ) + formatters = { + CitationEnum.GOST.name: GOSTCitationFormatter, + CitationEnum.APA.name: Formatter_APA, + } + + if citation not in formatters: + logger.error("Данный стиль не поддерживается") + + formatter = formatters[citation] + formatted_models = tuple(str(item) for item in formatter(models).format()) logger.info("Генерация выходного файла ...") Renderer(formatted_models).render(path_output) diff --git a/src/readers/reader.py b/src/readers/reader.py index 9007a80..3e61aa3 100644 --- a/src/readers/reader.py +++ b/src/readers/reader.py @@ -7,7 +7,7 @@ import openpyxl from openpyxl.workbook import Workbook -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, DissertationModel, ArticleNewspaperModel from logger import get_logger from readers.base import BaseReader @@ -90,6 +90,57 @@ def attributes(self) -> dict: } +class DissertationReader(BaseReader): + """ + Чтение модели Диссертации. + """ + + @property + def model(self) -> Type[DissertationModel]: + return DissertationModel + + @property + def sheet(self) -> str: + return "Диссертация" + + @property + def attributes(self) -> dict: + return { + "author": {0: str}, + "title": {1: str}, + "academic_degree": {2: str}, + "science": {3: str}, + "specialty_code": {4: str}, + "publication_city": {5: str}, + "publication_year": {6: int}, + "page_count": {7: int}, + } + +class ArticleNewspaperReader(BaseReader): + """ + Чтение модели Статьи из газеты. + """ + + @property + def model(self) -> Type[ArticleNewspaperModel]: + return ArticleNewspaperModel + + @property + def sheet(self) -> str: + return "Статья из газеты" + + @property + def attributes(self) -> dict: + return { + "authors": {0: str}, + "article_title": {1: str}, + "newspaper_name": {2: str}, + "publication_year": {3: int}, + "publication_date": {4: str}, + "аrticle_number": {5: int}, + } + + class SourcesReader: """ Чтение из источника данных. @@ -100,6 +151,9 @@ class SourcesReader: BookReader, InternetResourceReader, ArticlesCollectionReader, + DissertationReader, + ArticleNewspaperReader, + ] def __init__(self, path: str) -> None: diff --git a/src/tests/formatters/test_form.py b/src/tests/formatters/test_form.py new file mode 100644 index 0000000..3cf8f74 --- /dev/null +++ b/src/tests/formatters/test_form.py @@ -0,0 +1,67 @@ +""" +Тестирование функций оформления списка источников по APA +""" + +from formatters.base import BaseCitationFormatter +from formatters.models import BookModel, InternetResourceModel +from formatters.styles.apa import Book_APA, InternetResource_APA + + +class Test_APA: + """ + Тестирование оформления списка источников по стандарту APA + """ + + def test_book(self, book_model_fixture: BookModel) -> None: + """ + Тестирование форматирования книг по стандарту APA. + :param BookModel book_model_fixture: Фикстура модели книги + :return: + """ + + model = Book_APA(book_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. (2020). Наука как искусство (3-е изд.). Просвещение." + ) + + def test_internet_resource( + self, internet_resource_model_fixture: InternetResourceModel + ) -> None: + """ + Тестирование форматирования интернет-ресурса по стандарту APA. + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :return: + """ + + model = InternetResource_APA(internet_resource_model_fixture) + + assert ( + model.formatted + == "Наука как искусство. (n.d.). Ведомости. Retrieved 01.01.2021, from https://www.vedomosti.ru" + ) + + def test_citation_formatter( + self, + book_model_fixture: BookModel, + internet_resource_model_fixture: InternetResourceModel, + ) -> None: + """ + Тестирование функции итогового форматирования списка источников. + :param BookModel book_model_fixture: Фикстура модели книги + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :param ArticlesCollectionModel articles_collection_model_fixture: Фикстура модели сборника статей + + :return: + """ + + models = [ + Book_APA(book_model_fixture), + InternetResource_APA(internet_resource_model_fixture), + ] + result = BaseCitationFormatter(models).format() + + # тестирование сортировки списка источников + assert result[0] == models[0] + assert result[1] == models[1] \ No newline at end of file diff --git a/src/tests/formatters/test_gost.py b/src/tests/formatters/test_gost.py index c93e1e7..55f9a5a 100644 --- a/src/tests/formatters/test_gost.py +++ b/src/tests/formatters/test_gost.py @@ -27,9 +27,7 @@ def test_book(self, book_model_fixture: BookModel) -> None: == "Иванов И.М., Петров С.Н. Наука как искусство. – 3-е изд. – СПб.: Просвещение, 2020. – 999 с." ) - def test_internet_resource( - self, internet_resource_model_fixture: InternetResourceModel - ) -> None: + def test_internet_resource(self, internet_resource_model_fixture: InternetResourceModel) -> None: """ Тестирование форматирования интернет-ресурса. diff --git a/src/tests/readers/test_readers.py b/src/tests/readers/test_readers.py index 67d863b..9cfc128 100644 --- a/src/tests/readers/test_readers.py +++ b/src/tests/readers/test_readers.py @@ -5,12 +5,15 @@ import pytest -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, DissertationModel, ArticleNewspaperModel from readers.reader import ( BookReader, SourcesReader, InternetResourceReader, ArticlesCollectionReader, + DissertationReader, + ArticleNewspaperReader, + ) from settings import TEMPLATE_FILE_PATH @@ -47,9 +50,9 @@ def test_book(self, workbook: Any) -> None: assert model.authors == "Иванов И.М., Петров С.Н." assert model.title == "Наука как искусство" assert model.edition == "3-е" - assert model.city == "СПб." + assert model.publication_city == "СПб." assert model.publishing_house == "Просвещение" - assert model.year == 2020 + assert model.publication_year == 2020 assert model.pages == 999 # проверка общего количества атрибутов @@ -96,14 +99,67 @@ def test_articles_collection(self, workbook: Any) -> None: assert model.authors == "Иванов И.М., Петров С.Н." assert model.article_title == "Наука как искусство" assert model.collection_title == "Сборник научных трудов" - assert model.city == "СПб." + assert model.publication_city == "СПб." assert model.publishing_house == "АСТ" - assert model.year == 2020 + assert model.publication_year == 2020 assert model.pages == "25-30" # проверка общего количества атрибутов assert len(model_type.schema().get("properties", {}).keys()) == 7 + def test_dissertation(self, workbook: Any) -> None: + """ + Тестирование чтения диссертации. + :param workbook: Объект тестовой рабочей книги. + """ + + models = DissertationReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = DissertationModel + + assert isinstance(model, model_type) + assert model.author == "Иванов И.М." + assert model.title == "Наука как искусство" + assert model.academic_degree == "д-р. / канд." + assert model.science == "экон." + assert model.specialty_code == "01.01.01" + assert model.publication_city == "СПб." + assert model.publication_year == 2020 + assert model.page_count == 199 + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 8 + + def test_newspaper(self, workbook: Any) -> None: + """ + Тестирование чтения cтатьи из газеты. + :param workbook: Объект тестовой рабочей книги. + """ + + models = ArticleNewspaperReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = ArticleNewspaperModel + + assert isinstance(model, model_type) + assert model.authors == "Иванов И.М., Петров С.Н." + assert model.article_title == "Наука как искусство" + assert model.newspaper_name == "Южный Урал" + assert model.publication_year == 1980 + assert model.publication_date == "01.10" + assert model.аrticle_number == 5 + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 6 + + + + def test_sources_reader(self) -> None: """ Тестирование функции чтения всех моделей из источника. @@ -111,7 +167,7 @@ def test_sources_reader(self) -> None: models = SourcesReader(TEMPLATE_FILE_PATH).read() # проверка общего считанного количества моделей - assert len(models) == 8 + assert len(models) == 10 # проверка наличия всех ожидаемых типов моделей среди типов считанных моделей model_types = {model.__class__.__name__ for model in models} @@ -119,4 +175,7 @@ def test_sources_reader(self) -> None: BookModel.__name__, InternetResourceModel.__name__, ArticlesCollectionModel.__name__, + DissertationModel.__name__, + ArticleNewspaperModel.__name__, + }