diff --git a/papermerge/core/features/document/router.py b/papermerge/core/features/document/router.py index 1aed5134e..81521f253 100644 --- a/papermerge/core/features/document/router.py +++ b/papermerge/core/features/document/router.py @@ -17,7 +17,7 @@ OrderBy, ) from papermerge.core.config import get_settings, FileServer -from papermerge.core.utils.decorators import skip_in_tests +from papermerge.core.utils.decorators import if_redis_present from papermerge.core.types import OrderEnum, PaginatedResponse @@ -151,7 +151,7 @@ def upload_file( return doc -@skip_in_tests +@if_redis_present def send_task(*args, **kwargs): logger.debug(f"Send task {args} {kwargs}") celery_app.send_task(*args, **kwargs) diff --git a/papermerge/core/features/document_types/db/api.py b/papermerge/core/features/document_types/db/api.py index 20d792a28..d7c49b8b6 100644 --- a/papermerge/core/features/document_types/db/api.py +++ b/papermerge/core/features/document_types/db/api.py @@ -9,7 +9,7 @@ from papermerge.core import schema from papermerge.core import constants as const from papermerge.core import orm -from papermerge.core.utils.decorators import skip_in_tests +from papermerge.core.utils.decorators import if_redis_present from .orm import DocumentType @@ -146,7 +146,7 @@ def update_document_type( return result -@skip_in_tests +@if_redis_present def send_task(*args, **kwargs): logger.debug(f"Send task {args} {kwargs}") celery_app.send_task(*args, **kwargs) diff --git a/papermerge/core/features/nodes/router.py b/papermerge/core/features/nodes/router.py index 5ffa5e32e..869b58e5a 100644 --- a/papermerge/core/features/nodes/router.py +++ b/papermerge/core/features/nodes/router.py @@ -16,7 +16,7 @@ from papermerge.core.features.nodes.db import api as nodes_dbapi from papermerge.core.routers.common import OPEN_API_GENERIC_JSON_DETAIL from papermerge.core.routers.params import CommonQueryParams -from papermerge.core.utils.decorators import skip_in_tests +from papermerge.core.utils.decorators import if_redis_present from papermerge.core.exceptions import EntityNotFound @@ -389,7 +389,7 @@ def remove_node_tags( return node -@skip_in_tests +@if_redis_present def _notify_index(node_id: uuid.UUID): id_as_str = str(node_id) # just in case, make sure it is str current_app.send_task( diff --git a/papermerge/core/features/page_mngm/db/api.py b/papermerge/core/features/page_mngm/db/api.py index 0e2997eae..1d3f75c45 100644 --- a/papermerge/core/features/page_mngm/db/api.py +++ b/papermerge/core/features/page_mngm/db/api.py @@ -13,7 +13,7 @@ from papermerge.core import constants as const from papermerge.core.pathlib import abs_page_path from papermerge.core.storage import get_storage_instance -from papermerge.core.utils.decorators import skip_in_tests +from papermerge.core.utils.decorators import if_redis_present from papermerge.core.db import Session from papermerge.core import orm, schema, types from papermerge.core.features.document.db import api as doc_dbapi @@ -683,7 +683,7 @@ def copy_without_pages( ] -@skip_in_tests +@if_redis_present def notify_version_update(add_ver_id: str, remove_ver_id: str): # Send tasks to the index to remove/add pages current_app.send_task(const.INDEX_UPDATE, (add_ver_id, remove_ver_id)) @@ -700,7 +700,7 @@ def notify_version_update(add_ver_id: str, remove_ver_id: str): ) -@skip_in_tests +@if_redis_present def notify_add_docs(add_doc_ids: List[str]): # send task to index logger.debug(f"Sending task {const.INDEX_ADD_DOCS} with {add_doc_ids}") @@ -718,7 +718,7 @@ def notify_add_docs(add_doc_ids: List[str]): ) -@skip_in_tests +@if_redis_present def notify_generate_previews(doc_id: list[str] | str): if isinstance(doc_id, str): current_app.send_task( diff --git a/papermerge/core/models/folder.py b/papermerge/core/models/folder.py index bdd1542a4..6ecdcd5ff 100644 --- a/papermerge/core/models/folder.py +++ b/papermerge/core/models/folder.py @@ -6,7 +6,7 @@ from papermerge.core import constants as const from papermerge.core.models import utils from papermerge.core.models.node import BaseTreeNode -from papermerge.core.utils.decorators import skip_in_tests +from papermerge.core.utils.decorators import if_redis_present class FolderManager(models.Manager): @@ -35,19 +35,15 @@ def delete(self, *args, **kwargs): self.publish_post_delete_task(deleted_node_ids) - @skip_in_tests + @if_redis_present def publish_post_delete_task(self, node_ids: List[str]): current_app.send_task( - const.INDEX_REMOVE_NODE, - kwargs={'item_ids': node_ids}, - route_name='i3' + const.INDEX_REMOVE_NODE, kwargs={"item_ids": node_ids}, route_name="i3" ) def get_by_breadcrumb( - self, - breadcrumb: str, # should this be pathlib.PurePath ? - user - ) -> 'Folder': + self, breadcrumb: str, user # should this be pathlib.PurePath ? + ) -> "Folder": """ Returns ``Folder`` instance of the node defined by given breadcrumb path of specific ``User``. @@ -57,11 +53,7 @@ def get_by_breadcrumb( folder = Folder.objects.get_by_breadcrumb('.home/My Documents', user) assert folder.title == 'My Documents' """ - return utils.get_by_breadcrumb( - Folder, - breadcrumb, - user - ) + return utils.get_by_breadcrumb(Folder, breadcrumb, user) CustomFolderManager = FolderManager.from_queryset(FolderQuerySet) @@ -91,7 +83,7 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) self.publish_post_save_task() - @skip_in_tests # skip this method when running tests + @if_redis_present # skip this method when running tests def publish_post_save_task(self): """Send task to worker to add folder changes to search index @@ -99,9 +91,7 @@ def publish_post_save_task(self): """ id_as_str = str(self.pk) current_app.send_task( - const.INDEX_ADD_NODE, - kwargs={'node_id': id_as_str}, - route_name='i3' + const.INDEX_ADD_NODE, kwargs={"node_id": id_as_str}, route_name="i3" ) class Meta: @@ -121,9 +111,6 @@ def get_inbox_children(user): Returns a ``QuerySet`` containing the immediate children nodes of given user's inbox folder """ - inbox_node = BaseTreeNode.objects.get( - title=Folder.INBOX_TITLE, - user=user - ) + inbox_node = BaseTreeNode.objects.get(title=Folder.INBOX_TITLE, user=user) return inbox_node.get_children() diff --git a/papermerge/core/models/node.py b/papermerge/core/models/node.py index 32418a49a..576d63be2 100644 --- a/papermerge/core/models/node.py +++ b/papermerge/core/models/node.py @@ -10,12 +10,12 @@ from papermerge.core.constants import INDEX_ADD_NODE, INDEX_REMOVE_NODE from papermerge.core.models.tags import ColoredTag from papermerge.core.signal_definitions import node_post_move -from papermerge.core.utils.decorators import skip_in_tests +from papermerge.core.utils.decorators import if_redis_present from .utils import uuid2raw_str -NODE_TYPE_FOLDER = 'folder' -NODE_TYPE_DOCUMENT = 'document' +NODE_TYPE_FOLDER = "folder" +NODE_TYPE_DOCUMENT = "document" def move_node(source_node, target_node): @@ -30,9 +30,7 @@ def move_node(source_node, target_node): source_node.parent = target_node source_node.save() node_post_move.send( - sender=BaseTreeNode, - instance=source_node, - new_parent=target_node + sender=BaseTreeNode, instance=source_node, new_parent=target_node ) @@ -70,12 +68,13 @@ class PolymorphicTagManager(_TaggableManager): instance - in this case `BaseTreeNode`; because tags were added via Folder - they won't be found when looked up via `BaseTreeNode` (and vice versa). """ + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Instead of Document or Folder instances/models # always use associated BaseTreeNode instances/model - if hasattr(self.instance, 'basetreenode_ptr'): # Document or Folder + if hasattr(self.instance, "basetreenode_ptr"): # Document or Folder self.model = self.instance.basetreenode_ptr.__class__ self.instance = self.instance.basetreenode_ptr @@ -116,12 +115,10 @@ def delete(self, *args, **kwargs): self.publish_post_delete_task(deleted_item_ids) - @skip_in_tests + @if_redis_present def publish_post_delete_task(self, node_ids: List[str]): current_app.send_task( - INDEX_REMOVE_NODE, - kwargs={'item_ids': node_ids}, - route_name='i3' + INDEX_REMOVE_NODE, kwargs={"item_ids": node_ids}, route_name="i3" ) @@ -132,12 +129,12 @@ class BaseTreeNode(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4) parent = models.ForeignKey( - 'self', + "self", on_delete=models.CASCADE, blank=True, null=True, - related_name='children', - verbose_name='parent' + related_name="children", + verbose_name="parent", ) # shortcut - helps to figure out if node is either folder or document # without performing extra joins. This field may be empty. In such @@ -150,50 +147,30 @@ class BaseTreeNode(models.Model): (NODE_TYPE_DOCUMENT, NODE_TYPE_DOCUMENT), ), blank=True, - null=True + null=True, ) title = models.CharField( - "Title", - max_length=200, - validators=[validators.safe_character_validator] + "Title", max_length=200, validators=[validators.safe_character_validator] ) lang = models.CharField( - 'Language', - max_length=8, - blank=False, - null=False, - default='deu' + "Language", max_length=8, blank=False, null=False, default="deu" ) - user = models.ForeignKey( - 'User', - related_name='nodes', - on_delete=models.CASCADE - ) + user = models.ForeignKey("User", related_name="nodes", on_delete=models.CASCADE) - created_at = models.DateTimeField( - auto_now_add=True - ) - updated_at = models.DateTimeField( - auto_now=True - ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) - tags = TaggableManager( - through=ColoredTag, - manager=PolymorphicTagManager - ) + tags = TaggableManager(through=ColoredTag, manager=PolymorphicTagManager) # custom Manager + custom QuerySet objects = CustomNodeManager() @property def breadcrumb(self) -> List[Tuple[uuid.UUID, str]]: - return [ - (item.id, item.title) - for item in self.get_ancestors() - ] + return [(item.id, item.title) for item in self.get_ancestors()] @property def idified_title(self): @@ -205,7 +182,7 @@ def idified_title(self): output: "My Invoices-233453" """ - return f'{self.title}-{self.id}' + return f"{self.title}-{self.id}" @property def _type(self) -> str: @@ -247,7 +224,7 @@ def is_document(self) -> bool: def get_ancestors(self, include_self=True): """Returns all ancestors of the node""" - sql = ''' + sql = """ WITH RECURSIVE tree AS ( SELECT *, 0 as level FROM core_basetreenode WHERE id = %s UNION ALL @@ -255,32 +232,32 @@ def get_ancestors(self, include_self=True): FROM core_basetreenode, tree WHERE core_basetreenode.id = tree.parent_id ) - ''' + """ node_id = uuid2raw_str(self.pk) if include_self: - sql += 'SELECT * FROM tree ORDER BY level DESC' + sql += "SELECT * FROM tree ORDER BY level DESC" return BaseTreeNode.objects.raw(sql, [node_id]) - sql += 'SELECT * FROM tree WHERE NOT id = %s ORDER BY level DESC' + sql += "SELECT * FROM tree WHERE NOT id = %s ORDER BY level DESC" return BaseTreeNode.objects.raw(sql, [node_id, node_id]) def get_descendants(self, include_self=True): """Returns all descendants of the node""" - sql = ''' + sql = """ WITH RECURSIVE tree AS ( SELECT * FROM core_basetreenode WHERE id = %s UNION ALL SELECT core_basetreenode.* FROM core_basetreenode, tree WHERE core_basetreenode.parent_id = tree.id ) - ''' + """ node_id = uuid2raw_str(self.pk) if include_self: - sql += 'SELECT * FROM tree' + sql += "SELECT * FROM tree" return BaseTreeNode.objects.raw(sql, [node_id]) - sql += 'SELECT * FROM tree WHERE NOT id = %s' + sql += "SELECT * FROM tree WHERE NOT id = %s" return BaseTreeNode.objects.raw(sql, [node_id, node_id]) def save(self, *args, **kwargs): @@ -290,13 +267,11 @@ def save(self, *args, **kwargs): super().save(*args, **kwargs) self.publish_post_save_task() - @skip_in_tests + @if_redis_present def publish_post_save_task(self): id_as_str = str(self.pk) current_app.send_task( - INDEX_ADD_NODE, - kwargs={'node_id': id_as_str}, - route_name='i3' + INDEX_ADD_NODE, kwargs={"node_id": id_as_str}, route_name="i3" ) def delete(self, *args, **kwargs): @@ -333,12 +308,10 @@ def delete(self, *args, **kwargs): self.publish_post_delete_task(deleted_item_ids) - @skip_in_tests + @if_redis_present def publish_post_delete_task(self, node_ids: List[str]): current_app.send_task( - INDEX_REMOVE_NODE, - kwargs={'item_ids': node_ids}, - route_name='i3' + INDEX_REMOVE_NODE, kwargs={"item_ids": node_ids}, route_name="i3" ) class Meta: @@ -348,30 +321,27 @@ class Meta: # of view of users, the BaseNodeTree are just a list of documents. verbose_name = "Documents" verbose_name_plural = "Documents" - _icon_name = 'basetreenode' + _icon_name = "basetreenode" constraints = [ models.UniqueConstraint( - name='unique title per parent per user', - fields=('parent', 'title', 'user_id') + name="unique title per parent per user", + fields=("parent", "title", "user_id"), ), # Prohibit `title` duplicates when `parent_id` is NULL models.UniqueConstraint( - name='title_uniq_when_parent_is_null_per_user', - fields=('title', 'user_id'), - condition=Q(parent__isnull=True) - ) + name="title_uniq_when_parent_is_null_per_user", + fields=("title", "user_id"), + condition=Q(parent__isnull=True), + ), ] def __repr__(self): class_name = type(self).__name__ - return '{}({!r}, {!r})'.format(class_name, self.pk, self.title) + return "{}({!r}, {!r})".format(class_name, self.pk, self.title) def __str__(self): class_name = type(self).__name__ return "{}(pk={}, title='{}', ctype='{}')".format( - class_name, - self.pk, - self.title, - self.ctype + class_name, self.pk, self.title, self.ctype ) diff --git a/papermerge/core/utils/decorators.py b/papermerge/core/utils/decorators.py index fd84c4beb..c0f24799a 100644 --- a/papermerge/core/utils/decorators.py +++ b/papermerge/core/utils/decorators.py @@ -4,7 +4,7 @@ config = get_settings() -def skip_in_tests(orig_func): +def if_redis_present(orig_func): """Skip decorated function when `config.papermerge__redis__url` is None""" def inner(*args, **kwargs):