revision_protection.py 3.4KB

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