revision_protection.py 3.4KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. # -*- coding: utf-8 -*-
  2. from sqlalchemy.orm import Session
  3. from sqlalchemy import inspect
  4. from sqlalchemy.orm.unitofwork import UOWTransaction
  5. from transaction import TransactionManager
  6. from contextlib import contextmanager
  7. from tracim.exceptions import ContentRevisionDeleteError
  8. from tracim.exceptions import ContentRevisionUpdateError
  9. from tracim.exceptions import SameValueError
  10. from tracim.models.data import ContentRevisionRO
  11. from tracim.models.data import Content
  12. from tracim.models.meta import DeclarativeBase
  13. def prevent_content_revision_delete(
  14. session: Session,
  15. flush_context: UOWTransaction,
  16. instances: [DeclarativeBase]
  17. ) -> None:
  18. for instance in session.deleted:
  19. if isinstance(instance, ContentRevisionRO) \
  20. and instance.revision_id is not None:
  21. raise ContentRevisionDeleteError(
  22. "ContentRevision is not deletable. " +
  23. "You must make a new revision with" +
  24. "is_deleted set to True. Look at " +
  25. "tracim.model.new_revision context " +
  26. "manager to make a new revision"
  27. )
  28. class RevisionsIntegrity(object):
  29. """
  30. Simple static used class to manage a list with list of ContentRevisionRO
  31. who are allowed to be updated.
  32. When modify an already existing (understood have an identity in databse)
  33. ContentRevisionRO, if it's not in RevisionsIntegrity._updatable_revisions
  34. list, a ContentRevisionUpdateError thrown.
  35. This class is used by tracim.model.new_revision context manager.
  36. """
  37. _updatable_revisions = []
  38. @classmethod
  39. def add_to_updatable(cls, revision: 'ContentRevisionRO') -> None:
  40. if inspect(revision).has_identity:
  41. raise ContentRevisionUpdateError("ContentRevision is not updatable. %s already have identity." % revision) # nopep8
  42. if revision not in cls._updatable_revisions:
  43. cls._updatable_revisions.append(revision)
  44. @classmethod
  45. def remove_from_updatable(cls, revision: 'ContentRevisionRO') -> None:
  46. if revision in cls._updatable_revisions:
  47. cls._updatable_revisions.remove(revision)
  48. @classmethod
  49. def is_updatable(cls, revision: 'ContentRevisionRO') -> bool:
  50. return revision in cls._updatable_revisions
  51. @contextmanager
  52. def new_revision(
  53. session: Session,
  54. tm: TransactionManager,
  55. content: Content,
  56. force_create_new_revision: bool=False,
  57. ) -> Content:
  58. """
  59. Prepare context to update a Content. It will add a new updatable revision
  60. to the content.
  61. :param session: Database _session
  62. :param tm: TransactionManager
  63. :param content: Content instance to update
  64. :param force_create_new_revision: Decide if new_rev should or should not
  65. be forced.
  66. :return:
  67. """
  68. with session.no_autoflush:
  69. try:
  70. if force_create_new_revision \
  71. or inspect(content.revision).has_identity:
  72. content.new_revision()
  73. RevisionsIntegrity.add_to_updatable(content.revision)
  74. yield content
  75. except SameValueError or ValueError as e:
  76. # INFO - 20-03-2018 - renew transaction when error happened
  77. # This avoid bad _session data like new "temporary" revision
  78. # to be add when problem happen.
  79. tm.abort()
  80. tm.begin()
  81. raise e
  82. finally:
  83. RevisionsIntegrity.remove_from_updatable(content.revision)