diff --git a/src/formatters/models.py b/src/formatters/models.py index c9236ca..9d7f46f 100644 --- a/src/formatters/models.py +++ b/src/formatters/models.py @@ -78,3 +78,56 @@ class ArticlesCollectionModel(BaseModel): publishing_house: str year: int = Field(..., gt=0) pages: str + + +class AutoabstractModel(BaseModel): + """ + Модель автореферата: + + .. code-block:: + + AutoabstractModel( + author="Иванов И.М.", + dissertation_title="д-р. / канд.", + author_title="Сборник научных трудов", + science_field="экон.", + speciality_code="01.01.01", + city="СПб.", + year=2020, + page="199", + ) + """ + + author: str + dissertation_title: str + author_title: str + science_field: str + speciality_code: str + city: str + year: int = Field(..., gt=0) + page: int + + +class JournalArticleModel(BaseModel): + + """ + Модель статьи из журнала: + + .. code-block:: + + JournalArticleModel( + authors="Иванов И.М., Петров С.Н.", + article_title="Наука как искусство", + journal="Образование и наука", + year=2020, + issue="10", + pages="25-30", + ) + """ + + authors: str + article_title: str + journal: str + year: int = Field(..., gt=0) + issue: int = Field(..., gt=0) + pages: str diff --git a/src/formatters/styles/apa.py b/src/formatters/styles/apa.py new file mode 100644 index 0000000..5cf5d92 --- /dev/null +++ b/src/formatters/styles/apa.py @@ -0,0 +1,111 @@ +""" +American Psychological Association +""" +from string import Template + +from pydantic import BaseModel + +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AutoabstractModel, + JournalArticleModel, +) +from formatters.styles.base import BaseCitationStyle +from logger import get_logger + + +logger = get_logger(__name__) + + +class APABook(BaseCitationStyle): + """ + Форматирование для книг: + """ + + data: BookModel + + @property + def template(self) -> Template: + return Template( + "$authors ($year) $title ($edition) $city: $publishing_house, $pages с." + ) + + 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.city, + publishing_house=self.data.publishing_house, + year=self.data.year, + pages=self.data.pages, + ) + + def get_edition(self) -> str: + """ + Получение отформатированной информации об издательстве. + + :return: Информация об издательстве. + """ + + return f"{self.data.edition} изд. – " if self.data.edition else "" + + +class APAInternetResource(BaseCitationStyle): + """ + Форматирование для интернет-ресурса: + """ + + data: InternetResourceModel + + @property + def template(self) -> Template: + return Template("$website ($access_date) $article $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 APACitationFormatter: + """ + Базовый класс для итогового форматирования списка источников. + """ + + formatters_map = { + BookModel.__name__: APABook, + InternetResourceModel.__name__: APAInternetResource, + } + + 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)) # type: ignore + + self.formatted_items = formatted_items + + def format(self) -> list[BaseCitationStyle]: + """ + Форматирование списка источников. + :return: + """ + + return sorted(self.formatted_items, key=lambda item: item.formatted) diff --git a/src/formatters/styles/gost.py b/src/formatters/styles/gost.py index b237f8a..f51d94b 100644 --- a/src/formatters/styles/gost.py +++ b/src/formatters/styles/gost.py @@ -5,7 +5,13 @@ from pydantic import BaseModel -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AutoabstractModel, + JournalArticleModel, +) from formatters.styles.base import BaseCitationStyle from logger import get_logger @@ -103,6 +109,63 @@ def substitute(self) -> str: ) +class GOSTAutoabstract(BaseCitationStyle): + """ + Форматирование для автореферата. + """ + + data: AutoabstractModel + + @property + def template(self) -> Template: + return Template( + "$author $dissertation_title: $author_title $science_field: $speciality_code $city $year. $page c." + ) + + def substitute(self) -> str: + logger.info( + 'Форматирование автореферата "%s" ...', self.data.dissertation_title + ) + + return self.template.substitute( + author=self.data.author, + dissertation_title=self.data.dissertation_title, + author_title=self.data.author_title, + science_field=self.data.science_field, + speciality_code=self.data.speciality_code, + city=self.data.city, + year=self.data.year, + page=self.data.page, + ) + + +class GOSTJournalArticle(BaseCitationStyle): + """ + Форматирование для статьи из журнала. + """ + + data: JournalArticleModel + + @property + def template(self) -> Template: + return Template( + "$authors $article_title // $journal. $year. № $issue. С. $pages." + ) + + 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, + journal=self.data.journal, + year=self.data.year, + issue=self.data.issue, + pages=self.data.pages, + ) + + class GOSTCitationFormatter: """ Базовый класс для итогового форматирования списка источников. @@ -112,6 +175,8 @@ class GOSTCitationFormatter: BookModel.__name__: GOSTBook, InternetResourceModel.__name__: GOSTInternetResource, ArticlesCollectionModel.__name__: GOSTCollectionArticle, + AutoabstractModel.__name__: GOSTAutoabstract, + JournalArticleModel.__name__: GOSTJournalArticle, } def __init__(self, models: list[BaseModel]) -> None: diff --git a/src/main.py b/src/main.py index 7a9fa8e..520abfd 100644 --- a/src/main.py +++ b/src/main.py @@ -5,6 +5,7 @@ import click +from formatters.styles.apa import APACitationFormatter from formatters.styles.gost import GOSTCitationFormatter from logger import get_logger from readers.reader import SourcesReader @@ -77,9 +78,16 @@ def process_input( ) models = SourcesReader(path_input).read() - formatted_models = tuple( - str(item) for item in GOSTCitationFormatter(models).format() - ) + + if citation == CitationEnum.GOST.name: + formatter_class = GOSTCitationFormatter + elif citation == CitationEnum.APA.name: + formatter_class = APACitationFormatter + else: + logger.error("Validation error : Unsupported input") + exit() + + formatted_models = tuple(str(item) for item in formatter_class(models).format()) logger.info("Генерация выходного файла ...") Renderer(formatted_models).render(path_output) diff --git a/src/readers/reader.py b/src/readers/reader.py index 9007a80..bdfc581 100644 --- a/src/readers/reader.py +++ b/src/readers/reader.py @@ -7,7 +7,13 @@ import openpyxl from openpyxl.workbook import Workbook -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AutoabstractModel, + JournalArticleModel, +) from logger import get_logger from readers.base import BaseReader @@ -90,6 +96,58 @@ def attributes(self) -> dict: } +class AutoabstractReader(BaseReader): + """ + Чтение модели автореферата. + """ + + @property + def model(self) -> Type[AutoabstractModel]: + return AutoabstractModel + + @property + def sheet(self) -> str: + return "Автореферат" + + @property + def attributes(self) -> dict: + return { + "author": {0: str}, + "dissertation_title": {1: str}, + "author_title": {2: str}, + "science_field": {3: str}, + "speciality_code": {4: str}, + "city": {5: str}, + "year": {6: int}, + "page": {7: int}, + } + + +class JournalArticleReader(BaseReader): + """ + Чтение модели статьи из журнала. + """ + + @property + def model(self) -> Type[JournalArticleModel]: + return JournalArticleModel + + @property + def sheet(self) -> str: + return "Статья из журнала" + + @property + def attributes(self) -> dict: + return { + "authors": {0: str}, + "article_title": {1: str}, + "journal": {2: str}, + "year": {3: int}, + "issue": {4: int}, + "pages": {5: str}, + } + + class SourcesReader: """ Чтение из источника данных. @@ -100,6 +158,8 @@ class SourcesReader: BookReader, InternetResourceReader, ArticlesCollectionReader, + AutoabstractReader, + JournalArticleReader, ] def __init__(self, path: str) -> None: diff --git a/src/settings.py b/src/settings.py index 62d2037..58050b4 100644 --- a/src/settings.py +++ b/src/settings.py @@ -7,6 +7,8 @@ # путь к файлу шаблона для создания входного файла TEMPLATE_FILE_PATH: str = os.getenv("TEMPLATE_FILE_PATH", "../media/template.xlsx") +# Стиль цитирования +# CITATION: # путь к входному файлу INPUT_FILE_PATH: str = os.getenv("INPUT_FILE_PATH", "../media/input.xlsx") # путь к выходному файлу diff --git a/src/tests/conftest.py b/src/tests/conftest.py index ac5c9aa..412fc39 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -3,7 +3,13 @@ """ import pytest -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AutoabstractModel, + JournalArticleModel, +) @pytest.fixture @@ -58,3 +64,41 @@ def articles_collection_model_fixture() -> ArticlesCollectionModel: year=2020, pages="25-30", ) + + +@pytest.fixture +def autoabstract_model_fixture() -> AutoabstractModel: + """ + Фикстура модели автореферата. + + :return: AutoabstractModel + """ + + return AutoabstractModel( + author="Иванов И.М.", + dissertation_title="Наука как искусство", + author_title="д-р. / канд.", + science_field="экон.", + speciality_code="01.01.01", + city="СПб.", + year=2020, + page=199, + ) + + +@pytest.fixture +def journal_article_model_fixture() -> JournalArticleModel: + """ + Фикстура модели статьи из журнала. + + :return: JournalArticleModel + """ + + return JournalArticleModel( + authors="Иванов И.М., Петров С.Н.", + article_title="Наука как искусство", + journal="Научный журнал", + year=2020, + issue=1, + pages="25-30", + ) diff --git a/src/tests/formatters/test_apa.py b/src/tests/formatters/test_apa.py new file mode 100644 index 0000000..53c97a2 --- /dev/null +++ b/src/tests/formatters/test_apa.py @@ -0,0 +1,77 @@ +""" +Тестирование функций оформления списка источников по APA +""" + +from formatters.base import BaseCitationFormatter +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AutoabstractModel, + JournalArticleModel, +) +from formatters.styles.apa import APABook, APAInternetResource + + +class TestAPA: + """ + Тестирование оформления списка источников согласно APA + """ + + def test_book(self, book_model_fixture: BookModel) -> None: + """ + Тестирование форматирования книги. + + :param BookModel book_model_fixture: Фикстура модели книги + :return: + """ + + model = APABook(book_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. (2020) Наука как искусство (3-е изд. – ) СПб.: Просвещение, 999 с." + ) + + def test_internet_resource( + self, internet_resource_model_fixture: InternetResourceModel + ) -> None: + """ + Тестирование форматирования интернет-ресурса. + + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :return: + """ + + model = APAInternetResource(internet_resource_model_fixture) + + assert ( + model.formatted + == "Ведомости (01.01.2021) Наука как искусство https://www.vedomosti.ru" + ) + + def test_citation_formatter( + self, + book_model_fixture: BookModel, + internet_resource_model_fixture: InternetResourceModel, + articles_collection_model_fixture: ArticlesCollectionModel, + autoabstract_model_fixture: AutoabstractModel, + journal_article_model_fixture: JournalArticleModel, + ) -> None: + """ + Тестирование функции итогового форматирования списка источников. + + :param BookModel book_model_fixture: Фикстура модели книги + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :return: + """ + + models = [ + APABook(book_model_fixture), + APAInternetResource(internet_resource_model_fixture), + ] + result = BaseCitationFormatter(models).format() + + # тестирование сортировки списка источников + assert result[0] == models[1] + assert result[1] == models[0] diff --git a/src/tests/formatters/test_gost.py b/src/tests/formatters/test_gost.py index c93e1e7..f34fd87 100644 --- a/src/tests/formatters/test_gost.py +++ b/src/tests/formatters/test_gost.py @@ -3,8 +3,20 @@ """ from formatters.base import BaseCitationFormatter -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel -from formatters.styles.gost import GOSTBook, GOSTInternetResource, GOSTCollectionArticle +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AutoabstractModel, + JournalArticleModel, +) +from formatters.styles.gost import ( + GOSTBook, + GOSTInternetResource, + GOSTCollectionArticle, + GOSTAutoabstract, + GOSTJournalArticle, +) class TestGOST: @@ -61,11 +73,45 @@ def test_articles_collection( == "Иванов И.М., Петров С.Н. Наука как искусство // Сборник научных трудов. – СПб.: АСТ, 2020. – С. 25-30." ) + def test_autoabstract(self, autoabstract_model_fixture: AutoabstractModel) -> None: + """ + Тестирование форматирования автореферата. + + :param AutoabstractModel autoabstract_model_fixture: Фикстура модели реферата + :return: + """ + + model = GOSTAutoabstract(autoabstract_model_fixture) + + assert ( + model.formatted + == "Иванов И.М. Наука как искусство: д-р. / канд. экон.: 01.01.01 СПб. 2020. 199 c." + ) + + def test_journal_article( + self, journal_article_model_fixture: JournalArticleModel + ) -> None: + """ + Тестирование форматирования статьи из журнала. + + :param JournalArticleModel journal_article_model_fixture: Фикстура модели статьи журнала + :return: + """ + + model = GOSTJournalArticle(journal_article_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. Наука как искусство // Научный журнал. 2020. № 1. С. 25-30." + ) + def test_citation_formatter( self, book_model_fixture: BookModel, internet_resource_model_fixture: InternetResourceModel, articles_collection_model_fixture: ArticlesCollectionModel, + autoabstract_model_fixture: AutoabstractModel, + journal_article_model_fixture: JournalArticleModel, ) -> None: """ Тестирование функции итогового форматирования списка источников. @@ -73,6 +119,8 @@ def test_citation_formatter( :param BookModel book_model_fixture: Фикстура модели книги :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса :param ArticlesCollectionModel articles_collection_model_fixture: Фикстура модели сборника статей + :param AutoabstractModel dissertation_fixture: Фикстура модели автореферата + :param JournalArticleModel journal_article_fixture: Фикстура модели статьи журнала :return: """ @@ -80,10 +128,14 @@ def test_citation_formatter( GOSTBook(book_model_fixture), GOSTInternetResource(internet_resource_model_fixture), GOSTCollectionArticle(articles_collection_model_fixture), + GOSTAutoabstract(autoabstract_model_fixture), + GOSTJournalArticle(journal_article_model_fixture), ] result = BaseCitationFormatter(models).format() # тестирование сортировки списка источников - assert result[0] == models[2] - assert result[1] == models[0] - assert result[2] == models[1] + assert result[0] == models[3] + assert result[1] == models[4] + assert result[2] == models[2] + assert result[3] == models[0] + assert result[4] == models[1] diff --git a/src/tests/readers/test_readers.py b/src/tests/readers/test_readers.py index 67d863b..4e32108 100644 --- a/src/tests/readers/test_readers.py +++ b/src/tests/readers/test_readers.py @@ -5,12 +5,20 @@ import pytest -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import ( + BookModel, + InternetResourceModel, + ArticlesCollectionModel, + AutoabstractModel, + JournalArticleModel, +) from readers.reader import ( BookReader, SourcesReader, InternetResourceReader, ArticlesCollectionReader, + AutoabstractReader, + JournalArticleReader, ) from settings import TEMPLATE_FILE_PATH @@ -104,6 +112,58 @@ def test_articles_collection(self, workbook: Any) -> None: # проверка общего количества атрибутов assert len(model_type.schema().get("properties", {}).keys()) == 7 + def test_autoabstract(self, workbook: Any) -> None: + """ + Тестирование чтения автореферата. + + :param workbook: Объект тестовой рабочей книги. + """ + + models = AutoabstractReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = AutoabstractModel + + assert isinstance(model, model_type) + assert model.author == "Иванов И.М." + assert model.dissertation_title == "Наука как искусство" + assert model.author_title == "д-р. / канд." + assert model.science_field == "экон." + assert model.speciality_code == "01.01.01" + assert model.city == "СПб." + assert model.year == 2020 + assert model.page == 199 + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 8 + + def test_journal_article(self, workbook: Any) -> None: + """ + Тестирование чтения статьи из журнала. + + :param workbook: Объект тестовой рабочей книги. + """ + + models = JournalArticleReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = JournalArticleModel + + assert isinstance(model, model_type) + assert model.authors == "Иванов И.М., Петров С.Н." + assert model.article_title == "Наука как искусство" + assert model.journal == "Образование и наука" + assert model.year == 2020 + assert model.issue == 10 + assert model.pages == "25-30" + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 6 + def test_sources_reader(self) -> None: """ Тестирование функции чтения всех моделей из источника. @@ -111,7 +171,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 +179,6 @@ def test_sources_reader(self) -> None: BookModel.__name__, InternetResourceModel.__name__, ArticlesCollectionModel.__name__, + AutoabstractModel.__name__, + JournalArticleModel.__name__, }