diff --git a/README.md b/README.md index 23fbc4c..35b1b69 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 7th ## Installation diff --git a/docs/source/index.rst b/docs/source/index.rst index 41d6226..b7fadf3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -7,6 +7,7 @@ Поддерживаемые стили цитирования: - ГОСТ Р 7.0.5-2008 + - APA 7th Установка ========= diff --git a/src/formatters/models.py b/src/formatters/models.py index c9236ca..648af68 100644 --- a/src/formatters/models.py +++ b/src/formatters/models.py @@ -78,3 +78,51 @@ class ArticlesCollectionModel(BaseModel): publishing_house: str year: int = Field(..., gt=0) pages: str +class DissertationModel(BaseModel): + + """ + Модель диссертации: + + .. code-block:: + + DissertationModel( + authors="Иванов И.М., Петров С.Н.", + desertation_title="Наука как искусство", + canddoc="канд.", + sience="экон." + code="01.01.01" + city="СПб.", + year=2020, + pages=999, + ) + """ + + authors: str + desertation_title: str + canddoc:str + sience: str + code:str + city: str + year: int = Field(..., gt=0) + pages: int = Field(..., gt=0) +class JournalArticleModel(BaseModel): + """ + Модель статьи из журнала: + + .. code-block:: + + MagazineArticleModel( + authors="Иванов И.М., Петров С.Н.", + article_title="Наука как искусство", + journal_title="Научный журнал", + year=2020, + journal_number=1, + pages="25-30", + ) + """ + authors: str + article_title: str + journal_title: str + year: int = Field(..., gt=0) + journal_number: int = Field(..., gt=0) + pages: str \ No newline at end of file diff --git a/src/formatters/styles/apa.py b/src/formatters/styles/apa.py new file mode 100644 index 0000000..78dd810 --- /dev/null +++ b/src/formatters/styles/apa.py @@ -0,0 +1,185 @@ +""" +Стиль цитирования по APA 7th. +""" +from string import Template + +from pydantic import BaseModel + +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, JournalArticleModel, DissertationModel +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. $publishing_house." + ) + + def substitute(self) -> str: + + logger.info('Форматирование книги "%s" ...', self.data.title) + + return self.template.substitute( + authors=self.data.authors, + title=self.data.title, + publishing_house=self.data.publishing_house, + year=self.data.year, + ) + + 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( + "$article (n.d.) $website $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, + ) + + +class APACollectionArticle(BaseCitationStyle): + """ + Форматирование для статьи из сборника. + """ + + data: ArticlesCollectionModel + + @property + def template(self) -> Template: + return Template( + "$authors ($year). $article_title. $collection_title, $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, + collection_title=self.data.collection_title, + city=self.data.city, + publishing_house=self.data.publishing_house, + year=self.data.year, + pages=self.data.pages, + ) +class APAJournalArticle(BaseCitationStyle): + """ + Форматирование для статьи из журнала. + """ + + data: JournalArticleModel + + @property + def template(self) -> Template: + return Template( + "$authors ($year). $article_title. $journal_title, $journal_number, $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_title=self.data.journal_title, + year=self.data.year, + journal_number = self.data.journal_number, + pages=self.data.pages, + ) +class APADissertation(BaseCitationStyle): + """ + Форматирование для статьи из сборника. + """ + + data: DissertationModel + + @property + def template(self) -> Template: + return Template( + "$authors ($year). $desertation_title [$canddoc диссиртация]" + ) + + def substitute(self) -> str: + + logger.info('Форматирование диссертации "%s" ...', self.data.desertation_title) + + return self.template.substitute( + authors=self.data.authors, + desertation_title=self.data.desertation_title, + canddoc=self.data.canddoc, + sience=self.data.sience, + code=self.data.code, + city=self.data.city, + year=self.data.year, + pages=self.data.pages, + ) +class APACitationFormatter: + """ + Базовый класс для итогового форматирования списка источников. + """ + + formatters_map = { + BookModel.__name__: APABook, + InternetResourceModel.__name__: APAInternetResource, + ArticlesCollectionModel.__name__: APACollectionArticle, + DissertationModel.__name__: APADissertation, + JournalArticleModel.__name__: APAJournalArticle, + } + + 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) \ No newline at end of file diff --git a/src/formatters/styles/gost.py b/src/formatters/styles/gost.py index b237f8a..6594e42 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, JournalArticleModel, DissertationModel from formatters.styles.base import BaseCitationStyle from logger import get_logger @@ -101,8 +101,58 @@ def substitute(self) -> str: year=self.data.year, pages=self.data.pages, ) +class GOSTJournalArticle(BaseCitationStyle): + """ + Форматирование для статьи из журнала. + """ + + data: JournalArticleModel + + @property + def template(self) -> Template: + return Template( + "$authors $article_title // $journal_title – $year. - №$journal_number, $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_title=self.data.journal_title, + year=self.data.year, + journal_number = self.data.journal_number, + pages=self.data.pages, + ) +class GOSTDissertation(BaseCitationStyle): + """ + Форматирование для статьи из сборника. + """ + + data: DissertationModel + + @property + def template(self) -> Template: + return Template( + "$authors, $desertation_title [Текст]: дис. ... $canddoc $sience наук: $code / $authors - $city, $year. - $pages с." + ) + + def substitute(self) -> str: + + logger.info('Форматирование диссертации "%s" ...', self.data.desertation_title) + + return self.template.substitute( + authors=self.data.authors, + desertation_title=self.data.desertation_title, + canddoc=self.data.canddoc, + sience=self.data.sience, + code=self.data.code, + city=self.data.city, + year=self.data.year, + pages=self.data.pages, + ) class GOSTCitationFormatter: """ Базовый класс для итогового форматирования списка источников. @@ -112,6 +162,8 @@ class GOSTCitationFormatter: BookModel.__name__: GOSTBook, InternetResourceModel.__name__: GOSTInternetResource, ArticlesCollectionModel.__name__: GOSTCollectionArticle, + DissertationModel.__name__: GOSTDissertation, + JournalArticleModel.__name__: GOSTJournalArticle, } def __init__(self, models: list[BaseModel]) -> None: diff --git a/src/main.py b/src/main.py index 7a9fa8e..f45866e 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 APACitationFormatter logger = get_logger(__name__) @@ -75,16 +76,24 @@ def process_input( path_input, path_output, ) - models = SourcesReader(path_input).read() - formatted_models = tuple( - str(item) for item in GOSTCitationFormatter(models).format() - ) - - logger.info("Генерация выходного файла ...") - Renderer(formatted_models).render(path_output) + match citation: + case CitationEnum.GOST.name: + formatted_models = tuple( + str(item) for item in GOSTCitationFormatter(models).format() + ) + logger.info("Генерация выходного файла GOST ...") + Renderer(formatted_models).render(path_output) + logger.info("Команда успешно завершена.") + case CitationEnum.APA.name: + formatted_models = tuple( + str(item) for item in APACitationFormatter(models).format() + ) + Renderer(formatted_models).render(path_output) + logger.info("Генерация выходного файла APA ...") + Renderer(formatted_models).render(path_output) + logger.info("Команда успешно завершена.") - logger.info("Команда успешно завершена.") if __name__ == "__main__": diff --git a/src/readers/reader.py b/src/readers/reader.py index 9007a80..26212c9 100644 --- a/src/readers/reader.py +++ b/src/readers/reader.py @@ -7,11 +7,12 @@ import openpyxl from openpyxl.workbook import Workbook -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, DissertationModel, JournalArticleModel from logger import get_logger from readers.base import BaseReader + logger = get_logger(__name__) @@ -88,6 +89,54 @@ def attributes(self) -> dict: "year": {5: int}, "pages": {6: str}, } +class DissertationReader(BaseReader): + """ + Чтение модели диссертаций. + """ + + @property + def model(self) -> Type[DissertationModel]: + return DissertationModel + + @property + def sheet(self) -> str: + return "Диссертация" + + @property + def attributes(self) -> dict: + return { + "authors": {0: str}, + "desertation_title": {1: str}, + "canddoc": {2: str}, + "sience": {3: str}, + "code": {4: str}, + "city": {5: str}, + "year": {6: int}, + "pages": {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_title": {2: str}, + "year": {3: int}, + "journal_number": {4: int}, + "pages": {5: str}, + } class SourcesReader: @@ -100,6 +149,8 @@ class SourcesReader: BookReader, InternetResourceReader, ArticlesCollectionReader, + JournalArticleReader, + DissertationReader, ] def __init__(self, path: str) -> None: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index ac5c9aa..39c7bf3 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -3,7 +3,7 @@ """ import pytest -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, JournalArticleModel, DissertationModel @pytest.fixture @@ -58,3 +58,37 @@ def articles_collection_model_fixture() -> ArticlesCollectionModel: year=2020, pages="25-30", ) +@pytest.fixture +def journal_articles_model_fixture() -> JournalArticleModel: + """ + Фикстура модели сборника статей. + + :return: JournalArticlesnModel + """ + + return JournalArticleModel( + authors="Иванов И.М., Петров С.Н.", + article_title="Наука как искусство", + journal_title = "Образование и наука", + year = 2020, + journal_number = 10, + pages = "25-30", + ) +@pytest.fixture +def disseratation_model_fixture() -> DissertationModel: + """ + Фикстура модели сборника статей. + + :return: DissertationModel + """ + + return DissertationModel( + authors="Иванов И.М.", + desertation_title="Наука как искусство", + canddoc="канд.", + sience="экон.", + code = "01.01.01", + city = "СПб.", + year = 2020, + pages = 199, + ) diff --git a/src/tests/formatters/test_apa.py b/src/tests/formatters/test_apa.py new file mode 100644 index 0000000..6c14411 --- /dev/null +++ b/src/tests/formatters/test_apa.py @@ -0,0 +1,126 @@ +""" +Тестирование функций оформления списка источников по APA 7th. +""" + +from formatters.base import BaseCitationFormatter +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, DissertationModel, JournalArticleModel +from formatters.styles.apa import APABook, APAInternetResource, APACollectionArticle, APADissertation, APAJournalArticle + + +class TestGOST: + """ + Тестирование оформления списка источников согласно APA 7th. + """ + + def test_book(self, book_model_fixture: BookModel) -> None: + """ + Тестирование форматирования книги. + + :param BookModel book_model_fixture: Фикстура модели книги + :return: + """ + + model = APABook(book_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. (2020). Наука как искусство. Просвещение." + ) + + 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 + == "Наука как искусство (n.d.) Ведомости https://www.vedomosti.ru" + ) + + def test_articles_collection( + self, articles_collection_model_fixture: ArticlesCollectionModel + ) -> None: + """ + Тестирование форматирования сборника статей. + + :param ArticlesCollectionModel articles_collection_model_fixture: Фикстура модели сборника статей + :return: + """ + + model = APACollectionArticle(articles_collection_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. (2020). Наука как искусство. Сборник научных трудов, 25-30." + ) + def test_journal_articles( + self, journal_articles_model_fixture: JournalArticleModel + ) -> None: + """ + Тестирование форматирования сборника статей. + + :param JournalArticleModel journal_articles_model_fixture: Фикстура модели сборника статей + :return: + """ + + model = APAJournalArticle(journal_articles_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. (2020). Наука как искусство. Образование и наука, 10, 25-30." + ) + def test_dissertation( + self, disseratation_model_fixture: DissertationModel + ) -> None: + """ + Тестирование форматирования диссертаций. + + :param DissertationModel disseratation_model_fixture: Фикстура модели диссертаций + :return: + """ + + model = APADissertation(disseratation_model_fixture) + + assert ( + model.formatted + == "Иванов И.М. (2020). Наука как искусство [канд. диссиртация]" + ) + def test_citation_formatter( + self, + book_model_fixture: BookModel, + internet_resource_model_fixture: InternetResourceModel, + articles_collection_model_fixture: ArticlesCollectionModel, + disseratation_model_fixture: DissertationModel, + journal_articles_model_fixture: JournalArticleModel + ) -> None: + """ + Тестирование функции итогового форматирования списка источников. + + :param BookModel book_model_fixture: Фикстура модели книги + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :param ArticlesCollectionModel articles_collection_model_fixture: Фикстура модели сборника статей + :return: + """ + + models = [ + APABook(book_model_fixture), + APAInternetResource(internet_resource_model_fixture), + APACollectionArticle(articles_collection_model_fixture), + APAJournalArticle(journal_articles_model_fixture), + APADissertation(disseratation_model_fixture), + ] + result = BaseCitationFormatter(models).format() + + # тестирование сортировки списка источников + assert result[0] == models[4] + assert result[1] == models[3] + assert result[2] == models[0] + assert result[3] == models[2] + assert result[4] == models[1] diff --git a/src/tests/formatters/test_gost.py b/src/tests/formatters/test_gost.py index c93e1e7..2b87722 100644 --- a/src/tests/formatters/test_gost.py +++ b/src/tests/formatters/test_gost.py @@ -3,8 +3,8 @@ """ 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, DissertationModel, JournalArticleModel +from formatters.styles.gost import GOSTBook, GOSTInternetResource, GOSTCollectionArticle, GOSTDissertation, GOSTJournalArticle class TestGOST: @@ -60,12 +60,46 @@ def test_articles_collection( model.formatted == "Иванов И.М., Петров С.Н. Наука как искусство // Сборник научных трудов. – СПб.: АСТ, 2020. – С. 25-30." ) + def test_journal_articles( + self, journal_articles_model_fixture: JournalArticleModel + ) -> None: + """ + Тестирование форматирования сборника статей. + + :param JournalArticleModel journal_articles_model_fixture: Фикстура модели сборника статей + :return: + """ + model = GOSTJournalArticle(journal_articles_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Петров С.Н. Наука как искусство // Образование и наука – 2020. - №10, 25-30." + ) + def test_dissertation( + self, disseratation_model_fixture: DissertationModel + ) -> None: + """ + Тестирование форматирования диссертаций. + + :param DissertationModel disseratation_model_fixture: Фикстура модели диссертаций + :return: + """ + + model = GOSTDissertation(disseratation_model_fixture) + + assert ( + model.formatted + == "Иванов И.М., Наука как искусство [Текст]: дис. ... канд. экон. наук: 01.01.01 / Иванов И.М. - СПб., 2020. - 199 с." + ) def test_citation_formatter( self, book_model_fixture: BookModel, internet_resource_model_fixture: InternetResourceModel, articles_collection_model_fixture: ArticlesCollectionModel, + disseratation_model_fixture: DissertationModel, + journal_articles_model_fixture: JournalArticleModel + ) -> None: """ Тестирование функции итогового форматирования списка источников. @@ -80,10 +114,14 @@ def test_citation_formatter( GOSTBook(book_model_fixture), GOSTInternetResource(internet_resource_model_fixture), GOSTCollectionArticle(articles_collection_model_fixture), + GOSTJournalArticle(journal_articles_model_fixture), + GOSTDissertation(disseratation_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[4] + assert result[1] == models[3] + assert result[2] == models[2] + assert result[3] == models[0] + assert result[4] == models[1] \ No newline at end of file diff --git a/src/tests/readers/test_readers.py b/src/tests/readers/test_readers.py index 67d863b..1b0ca9a 100644 --- a/src/tests/readers/test_readers.py +++ b/src/tests/readers/test_readers.py @@ -5,12 +5,14 @@ import pytest -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, DissertationModel, JournalArticleModel from readers.reader import ( BookReader, SourcesReader, InternetResourceReader, ArticlesCollectionReader, + DissertationReader, + JournalArticleReader, ) from settings import TEMPLATE_FILE_PATH @@ -104,6 +106,56 @@ def test_articles_collection(self, workbook: Any) -> None: # проверка общего количества атрибутов 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.authors == "Иванов И.М." + assert model.desertation_title == "Наука как искусство" + assert model.canddoc == "д-р. / канд." + assert model.sience == "экон." + assert model.code == "01.01.01" + assert model.city == "СПб." + assert model.year == 2020 + assert model.pages == 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_title == "Образование и наука" + assert model.year == 2020 + assert model.journal_number == 10 + assert model.pages == "25-30" + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 6 def test_sources_reader(self) -> None: """ Тестирование функции чтения всех моделей из источника. @@ -111,12 +163,14 @@ 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} assert model_types == { BookModel.__name__, InternetResourceModel.__name__, + DissertationModel.__name__, ArticlesCollectionModel.__name__, + JournalArticleModel.__name__ }