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..0d2941a 100644 --- a/src/formatters/models.py +++ b/src/formatters/models.py @@ -54,7 +54,6 @@ class InternetResourceModel(BaseModel): class ArticlesCollectionModel(BaseModel): - """ Модель сборника статей: @@ -78,3 +77,57 @@ class ArticlesCollectionModel(BaseModel): publishing_house: str year: int = Field(..., gt=0) pages: str + + +class RegulatoryActModel(BaseModel): + """ + Модель нормативного акта: + .. code-block:: + RegulatoryActModel( + type="Трудовой кодекс", + name="Наука как искусство", + agree_date="02.01.2022", + act_num="8888-88", + publishing_source="Сайт России", + year=2022, + source=5, + article=15, + amended_from="01.01.2021", + ) + """ + + type: str + name: str + agree_date: str + act_num: str + publishing_source: str + year: int = Field(..., gt=0) + source: int = Field(..., gt=0) + article: int = Field(..., gt=0) + amended_from: str + + +class DissertationModel(BaseModel): + """ + Модель диссертации: + .. code-block:: + DissertationModel( + author_name="Петров С.Н.", + title="Наука как искусство", + author_title="канд.", + special_field="ЭВМ.", + special_code="09.01.02", + city="СПб.", + year=2022, + pages=199, + ) + """ + + author_name: str = Field(..., min_length=1) + title: str = Field(..., min_length=1) + author_title: str = Field(..., min_length=1) + special_field: str = Field(..., min_length=1) + special_code: str = Field(..., min_length=1) + city: str = Field(..., min_length=1) + year: int = Field(..., gt=0) + pages: int = Field(..., gt=0) diff --git a/src/formatters/styles/apa.py b/src/formatters/styles/apa.py new file mode 100644 index 0000000..8e6ae6f --- /dev/null +++ b/src/formatters/styles/apa.py @@ -0,0 +1,197 @@ +""" +Стиль цитирования по APA 7. +""" +from string import Template + +from pydantic import BaseModel + +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, RegulatoryActModel, 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 $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.city, + publishing_house=self.data.publishing_house, + year=self.data.year, + ) + + def get_edition(self) -> str: + + + if self.data.edition: + ed = int(self.data.edition.split("-")[0]) + res = f"{ed}th" + if 10 <= ed % 100 <=19: + res = f"{ed}th" + elif ed % 10 == 1: + res = f"{ed}st" + elif ed % 10 == 2: + res = f"{ed}nd" + elif ed % 10 == 3: + res = f"{ed}rd" + return f" ({res} ed.)" + return "" + + +class APAInternetResource(BaseCitationStyle): + """ + Форматирование для интернет-ресурсов. + """ + + data: InternetResourceModel + + @property + def template(self) -> Template: + return Template( + '$article. $website. (n.d.). $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. (pp. $pages). $publishing_house.' + ) + + 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, + publishing_house=self.data.publishing_house, + year=self.data.year, + pages=self.data.pages, + ) + + + + + +class APARegulatoryAct(BaseCitationStyle): + """ + Форматирование для нормативного акта. + """ + + data: RegulatoryActModel + + @property + def template(self) -> Template: + return Template( + '$name, $act_num $publishing_source. § $article ($year).' + ) + + def substitute(self) -> str: + + logger.info('Форматирование нормативного акта "%s" ...', self.data.name) + + return self.template.substitute( + name=self.data.name, + publishing_source=self.data.publishing_source, + act_num=self.data.act_num, + article=self.data.article, + year=self.data.year, + ) + + +class APADissertation(BaseCitationStyle): + """ + Форматирование для диссертации. + """ + + data: DissertationModel + + @property + def template(self) -> Template: + return Template( + "$author_name ($year) $title, дис. [$author_title $special_field $special_code] $city, $pages с." + ) + + def substitute(self) -> str: + + logger.info('Форматирование диссертации "%s" ...', self.data.title) + + return self.template.substitute( + author_name=self.data.author_name, + title=self.data.title, + author_title=self.data.author_title, + special_field=self.data.special_field, + special_code=self.data.special_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, + RegulatoryActModel.__name__: APARegulatoryAct, + DissertationModel.__name__: APADissertation, + + } + + 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..6ba8471 100644 --- a/src/formatters/styles/gost.py +++ b/src/formatters/styles/gost.py @@ -5,11 +5,11 @@ from pydantic import BaseModel -from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, RegulatoryActModel, \ + DissertationModel from formatters.styles.base import BaseCitationStyle from logger import get_logger - logger = get_logger(__name__) @@ -27,7 +27,6 @@ def template(self) -> Template: ) def substitute(self) -> str: - logger.info('Форматирование книги "%s" ...', self.data.title) return self.template.substitute( @@ -64,7 +63,6 @@ def template(self) -> Template: ) def substitute(self) -> str: - logger.info('Форматирование интернет-ресурса "%s" ...', self.data.article) return self.template.substitute( @@ -89,7 +87,6 @@ def template(self) -> Template: ) def substitute(self) -> str: - logger.info('Форматирование сборника статей "%s" ...', self.data.article_title) return self.template.substitute( @@ -103,6 +100,63 @@ def substitute(self) -> str: ) +class GOSTRegulatoryAct(BaseCitationStyle): + """ + Форматирование для нормативного акта. + """ + + data: RegulatoryActModel + + @property + def template(self) -> Template: + return Template( + '$type "$name" от $agree_date №$act_num // $publishing_source, $year. – №$source – Ст. $article с изм. и допол. в ред. от $amended_from.' + ) + + def substitute(self) -> str: + logger.info('Форматирование нормативного акта "%s" ...', self.data.name) + + return self.template.substitute( + type=self.data.type, + name=self.data.name, + agree_date=self.data.agree_date, + act_num=self.data.act_num, + publishing_source=self.data.publishing_source, + year=self.data.year, + source=self.data.source, + article=self.data.article, + amended_from=self.data.amended_from, + ) + + +class GOSTDissertation(BaseCitationStyle): + """ + Форматирование для диссертации. + """ + + data: DissertationModel + + @property + def template(self) -> Template: + return Template( + "$author_name $title: дис. $author_title $special_field: $special_code $city $year. $pages c." + ) + + def substitute(self) -> str: + logger.info('Форматирование диссертации "%s" ...', self.data.title) + + return self.template.substitute( + author_name=self.data.author_name, + title=self.data.title, + author_title=self.data.author_title, + special_code=self.data.special_code, + special_field=self.data.special_field, + city=self.data.city, + year=self.data.year, + pages=self.data.pages + ) + + class GOSTCitationFormatter: """ Базовый класс для итогового форматирования списка источников. @@ -112,6 +166,8 @@ class GOSTCitationFormatter: BookModel.__name__: GOSTBook, InternetResourceModel.__name__: GOSTInternetResource, ArticlesCollectionModel.__name__: GOSTCollectionArticle, + RegulatoryActModel.__name__: GOSTRegulatoryAct, + DissertationModel.__name__: GOSTDissertation, } def __init__(self, models: list[BaseModel]) -> None: diff --git a/src/main.py b/src/main.py index 7a9fa8e..d07eb6e 100644 --- a/src/main.py +++ b/src/main.py @@ -6,6 +6,7 @@ import click from formatters.styles.gost import GOSTCitationFormatter +from formatters.styles.apa import APACitationFormatter from logger import get_logger from readers.reader import SourcesReader from renderer import Renderer @@ -21,7 +22,7 @@ class CitationEnum(Enum): """ GOST = "gost" # ГОСТ Р 7.0.5-2008 - MLA = "mla" # Modern Language Association + #MLA = "mla" # Modern Language Association APA = "apa" # American Psychological Association @@ -77,9 +78,15 @@ def process_input( ) models = SourcesReader(path_input).read() - formatted_models = tuple( - str(item) for item in GOSTCitationFormatter(models).format() - ) + formatter_mapping = { + CitationEnum.GOST.name: GOSTCitationFormatter, + CitationEnum.APA.name: APACitationFormatter + } + if citation not in formatter_mapping: + logger.error("Неподдерживаемый стиль") + + formatter_class = formatter_mapping[citation] + 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..96d827f 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, RegulatoryActModel,DissertationModel from logger import get_logger from readers.base import BaseReader @@ -90,6 +90,60 @@ def attributes(self) -> dict: } +class RegulatoryActReader(BaseReader): + """ + Чтение модели нормативного акта. + """ + + @property + def model(self) -> Type[RegulatoryActModel]: + return RegulatoryActModel + + @property + def sheet(self) -> str: + return " Закон, нормативный акт и т.п." + + @property + def attributes(self) -> dict: + return { + "type": {0: str}, + "name": {1: str}, + "agree_date": {2: str}, + "act_num": {3: str}, + "publishing_source": {4: str}, + "year": {5: int}, + "source": {6: int}, + "article": {7: int}, + "amended_from": {8: str}, + } + + +class DissertationReader(BaseReader): + """ + Чтение модели диссертации. + """ + @property + def model(self) -> Type[DissertationModel]: + return DissertationModel + + @property + def sheet(self) -> str: + return "Диссертация" + + @property + def attributes(self) -> dict: + return { + "author_name": {0: str}, + "title": {1: str}, + "author_title": {2: str}, + "special_code": {3: str}, + "special_field": {4: str}, + "city": {5: str}, + "year": {6: str}, + "pages": {7: str}, + } + + class SourcesReader: """ Чтение из источника данных. @@ -100,6 +154,8 @@ class SourcesReader: BookReader, InternetResourceReader, ArticlesCollectionReader, + DissertationReader, + RegulatoryActReader, ] def __init__(self, path: str) -> None: diff --git a/src/tests/conftest.py b/src/tests/conftest.py index ac5c9aa..4e0ce57 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,DissertationModel, RegulatoryActModel @pytest.fixture @@ -58,3 +58,41 @@ def articles_collection_model_fixture() -> ArticlesCollectionModel: year=2020, pages="25-30", ) + + +@pytest.fixture +def dissertation_fixture() -> DissertationModel: + """ + Фикстура модели диссертации. + :return: DissertationModel + """ + + return DissertationModel( + author_name="Иванов И.М.", + title="Наука как искусство", + author_title="д-р. / канд.", + special_code="01.01.01", + special_field="экон.", + city="СПб.", + year=2020, + pages=199, + ) + +@pytest.fixture +def regulatory_act_fixture() -> RegulatoryActModel: + """ + Фикстура модели нормативного акта. + :return: RegulatoryActModel + """ + + return RegulatoryActModel( + type="Трудовой кодекс", + name="Наука как искусство", + agree_date="02.01.2022", + act_num="8888-88", + publishing_source="Сайт России", + year=2022, + source=5, + article=15, + amended_from="01.01.2021", + ) \ No newline at end of file diff --git a/src/tests/formatters/test_apa.py b/src/tests/formatters/test_apa.py new file mode 100644 index 0000000..13b8015 --- /dev/null +++ b/src/tests/formatters/test_apa.py @@ -0,0 +1,121 @@ +from formatters.base import BaseCitationFormatter +from formatters.models import BookModel, InternetResourceModel, ArticlesCollectionModel, DissertationModel, RegulatoryActModel +from formatters.styles.apa import APABook, APAInternetResource, APACollectionArticle, APADissertation, APARegulatoryAct + + +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). Наука как искусство (3rd ed.). Просвещение." + ) + + 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). Наука как искусство, Сборник научных трудов. (pp. 25-30). АСТ." + ) + + def test_dissertation(self, dissertation_fixture: DissertationModel) -> None: + """ + Тестирование форматирования диссертации. + :param DissertationModel dissertation_fixture: Фикстура модели диссертации + :return: + """ + + model = APADissertation(dissertation_fixture) + + assert ( + model.formatted + == "Иванов И.М. (2020) Наука как искусство, дис. [д-р. / канд. экон. 01.01.01] СПб., 199 с." + ) + + + def test_regulatory_act( + self, regulatory_act_fixture: RegulatoryActModel + ) -> None: + """ + Тестирование форматирования сборника статей. + :param RegulatoryActModel regulatory_act_fixture: Фикстура модели сборника нормативных актов + :return: + """ + + model = APARegulatoryAct(regulatory_act_fixture) + + assert ( + model.formatted + == 'Наука как искусство, 8888-88 Сайт России. § 15 (2022).' + ) + + def test_citation_formatter( + self, + book_model_fixture: BookModel, + internet_resource_model_fixture: InternetResourceModel, + articles_collection_model_fixture: ArticlesCollectionModel, + dissertation_fixture: DissertationModel, + regulatory_act_fixture: RegulatoryActModel + + ) -> None: + """ + Тестирование функции итогового форматирования списка источников. + :param BookModel book_model_fixture: Фикстура модели книги + :param InternetResourceModel internet_resource_model_fixture: Фикстура модели интернет-ресурса + :param ArticlesCollectionModel articles_collection_model_fixture: Фикстура модели сборника статей + :param RegulatoryActModel regulatory_act_fixture: Фикстура модели норматитвного акта + :param DissertationModel dissertation_fixture: Фикстура модели диссертации + :return: + """ + + models = [ + APABook(book_model_fixture), + APAInternetResource(internet_resource_model_fixture), + APACollectionArticle(articles_collection_model_fixture), + APARegulatoryAct(regulatory_act_fixture), + APADissertation(dissertation_fixture) + ] + result = BaseCitationFormatter(models).format() + + # тестирование сортировки списка источников + assert result[0] == models[4] + assert result[1] == models[2] + assert result[2] == models[0] + assert result[3] == models[3] + assert result[4] == 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..c3a67dd 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, RegulatoryActModel +from formatters.styles.gost import GOSTBook, GOSTInternetResource, GOSTCollectionArticle, GOSTDissertation, GOSTRegulatoryAct class TestGOST: @@ -61,11 +61,43 @@ def test_articles_collection( == "Иванов И.М., Петров С.Н. Наука как искусство // Сборник научных трудов. – СПб.: АСТ, 2020. – С. 25-30." ) + def test_regulatory_act( + self, regulatory_act_fixture: RegulatoryActModel + ) -> None: + """ + Тестирование форматирования сборника статей. + :param RegulatoryActModel regulatory_act_fixture: Фикстура модели нормативных актов + :return: + """ + + model = GOSTRegulatoryAct(regulatory_act_fixture) + + assert ( + model.formatted + == 'Трудовой кодекс "Наука как искусство" от 02.01.2022 №8888-88 // Сайт России, 2022. – №5 – Ст. 15 с изм. и допол. в ред. от 01.01.2021.' + ) + + def test_dissertation(self, dissertation_fixture: DissertationModel) -> None: + """ + Тестирование форматирования диссертации. + :param DissertationModel dissertation_fixture: Фикстура модели диссертации + :return: + """ + + model = GOSTDissertation(dissertation_fixture) + + assert ( + model.formatted + == "Иванов И.М. Наука как искусство: дис. д-р. / канд. экон.: 01.01.01 СПб. 2020. 199 c." + ) + def test_citation_formatter( self, book_model_fixture: BookModel, internet_resource_model_fixture: InternetResourceModel, articles_collection_model_fixture: ArticlesCollectionModel, + dissertation_fixture: DissertationModel, + regulatory_act_fixture: RegulatoryActModel ) -> None: """ Тестирование функции итогового форматирования списка источников. @@ -80,10 +112,14 @@ def test_citation_formatter( GOSTBook(book_model_fixture), GOSTInternetResource(internet_resource_model_fixture), GOSTCollectionArticle(articles_collection_model_fixture), + GOSTDissertation(dissertation_fixture), + GOSTRegulatoryAct(regulatory_act_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[2] + assert result[2] == models[0] + assert result[3] == models[1] + assert result[4] == models[4] diff --git a/src/tests/readers/test_readers.py b/src/tests/readers/test_readers.py index 67d863b..fcda3c1 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, RegulatoryActModel from readers.reader import ( BookReader, SourcesReader, InternetResourceReader, ArticlesCollectionReader, + DissertationReader, + RegulatoryActReader ) from settings import TEMPLATE_FILE_PATH @@ -104,6 +106,59 @@ 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.author_name == "Иванов И.М." + assert model.title == "Наука как искусство" + assert model.author_title == "д-р. / канд." + assert model.special_field == "01.01.01" + assert model.special_code == "экон." + assert model.city == "СПб." + assert model.year == 2020 + assert model.pages == 199 + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 8 + + def test_regulatory_act(self, workbook: Any) -> None: + """ + Тестирование чтения нормативных актов. + :param workbook: Объект тестовой рабочей книги. + """ + + models = RegulatoryActReader(workbook).read() + + assert len(models) == 1 + model = models[0] + + model_type = RegulatoryActModel + + assert isinstance(model, model_type) + assert model.type == "Конституция Российской Федерации" + assert model.name == "Наука как искусство" + assert model.agree_date == "2000-01-01 00:00:00" + assert model.act_num == "1234-56" + assert model.publishing_source == "Парламентская газета" + assert model.year == 2020 + assert model.source == 5 + assert model.article == 15 + assert model.amended_from == "2002-09-11 00:00:00" + + # проверка общего количества атрибутов + assert len(model_type.schema().get("properties", {}).keys()) == 9 + def test_sources_reader(self) -> None: """ Тестирование функции чтения всех моделей из источника. @@ -111,7 +166,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 +174,6 @@ def test_sources_reader(self) -> None: BookModel.__name__, InternetResourceModel.__name__, ArticlesCollectionModel.__name__, + DissertationModel.__name__, + RegulatoryActModel.__name__ }