__init__.py 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  1. # -*- coding: utf-8 -*-
  2. """The application's model objects"""
  3. from contextlib import contextmanager
  4. from sqlalchemy import event, inspect, MetaData
  5. from sqlalchemy.ext.declarative import declarative_base
  6. from sqlalchemy.orm import scoped_session, sessionmaker, Session
  7. from sqlalchemy.orm.unitofwork import UOWTransaction
  8. from zope.sqlalchemy import ZopeTransactionExtension
  9. from tracim.lib.exception import ContentRevisionUpdateError, ContentRevisionDeleteError
  10. from tracim.lib.utils import SameValueError
  11. import transaction
  12. class RevisionsIntegrity(object):
  13. """
  14. Simple static used class to manage a list with list of ContentRevisionRO who are allowed to be updated.
  15. When modify an already existing (understood have an identity in databse) ContentRevisionRO, if it's not in
  16. RevisionsIntegrity._updatable_revisions list, a ContentRevisionUpdateError thrown.
  17. This class is used by tracim.model.new_revision context manager.
  18. """
  19. _updatable_revisions = []
  20. @classmethod
  21. def add_to_updatable(cls, revision: 'ContentRevisionRO') -> None:
  22. if inspect(revision).has_identity:
  23. raise ContentRevisionUpdateError("ContentRevision is not updatable. %s already have identity." % revision)
  24. if revision not in cls._updatable_revisions:
  25. cls._updatable_revisions.append(revision)
  26. @classmethod
  27. def remove_from_updatable(cls, revision: 'ContentRevisionRO') -> None:
  28. if revision in cls._updatable_revisions:
  29. cls._updatable_revisions.remove(revision)
  30. @classmethod
  31. def is_updatable(cls, revision: 'ContentRevisionRO') -> bool:
  32. return revision in cls._updatable_revisions
  33. # Global session manager: DBSession() returns the Thread-local
  34. # session object appropriate for the current web request.
  35. maker = sessionmaker(
  36. autoflush=True,
  37. autocommit=False,
  38. extension=ZopeTransactionExtension(),
  39. expire_on_commit=False,
  40. )
  41. DBSession = scoped_session(maker)
  42. # Base class for all of our model classes: By default, the data model is
  43. # defined with SQLAlchemy's declarative extension, but if you need more
  44. # control, you can switch to the traditional method.
  45. convention = {
  46. "ix": 'ix__%(column_0_label)s', # Indexes
  47. "uq": "uq__%(table_name)s__%(column_0_name)s", # Unique constrains
  48. "fk": "fk__%(table_name)s__%(column_0_name)s__%(referred_table_name)s", # Foreign keys
  49. "pk": "pk__%(table_name)s" # Primary keys
  50. }
  51. metadata = MetaData(naming_convention=convention)
  52. DeclarativeBase = declarative_base(metadata=metadata)
  53. # There are two convenient ways for you to spare some typing.
  54. # You can have a query property on all your model classes by doing this:
  55. # DeclarativeBase.query = DBSession.query_property()
  56. # Or you can use a session-aware mapper as it was used in TurboGears 1:
  57. # DeclarativeBase = declarative_base(mapper=DBSession.mapper)
  58. # Global metadata.
  59. # The default metadata is the one from the declarative base.
  60. metadata = DeclarativeBase.metadata
  61. # If you have multiple databases with overlapping table names, you'll need a
  62. # metadata for each database. Feel free to rename 'metadata2'.
  63. #metadata2 = MetaData()
  64. #####
  65. # Generally you will not want to define your table's mappers, and data objects
  66. # here in __init__ but will want to create modules them in the model directory
  67. # and import them at the bottom of this file.
  68. #
  69. ######
  70. def init_model(engine):
  71. """Call me before using any of the tables or classes in the model."""
  72. if not DBSession.registry.has(): # Prevent a SQLAlchemy warning
  73. DBSession.configure(bind=engine)
  74. # If you are using reflection to introspect your database and create
  75. # table objects for you, your tables must be defined and mapped inside
  76. # the init_model function, so that the engine is available if you
  77. # use the model outside tg2, you need to make sure this is called before
  78. # you use the model.
  79. #
  80. # See the following example:
  81. #global t_reflected
  82. #t_reflected = Table("Reflected", metadata,
  83. # autoload=True, autoload_with=engine)
  84. #mapper(Reflected, t_reflected)
  85. # Import your model modules here.
  86. from tracim.model.auth import User, Group, Permission
  87. from tracim.model.data import Content, ContentRevisionRO
  88. @event.listens_for(DBSession, 'before_flush')
  89. def prevent_content_revision_delete(session: Session, flush_context: UOWTransaction,
  90. instances: [DeclarativeBase]) -> None:
  91. for instance in session.deleted:
  92. if isinstance(instance, ContentRevisionRO) and instance.revision_id is not None:
  93. raise ContentRevisionDeleteError("ContentRevision is not deletable. You must make a new revision with" +
  94. "is_deleted set to True. Look at tracim.model.new_revision context " +
  95. "manager to make a new revision")
  96. @contextmanager
  97. def new_revision(
  98. content: Content,
  99. force_create_new_revision: bool=False,
  100. ) -> Content:
  101. """
  102. Prepare context to update a Content. It will add a new updatable revision to the content.
  103. :param content: Content instance to update
  104. :return:
  105. """
  106. with DBSession.no_autoflush:
  107. try:
  108. if force_create_new_revision \
  109. or inspect(content.revision).has_identity:
  110. content.new_revision()
  111. RevisionsIntegrity.add_to_updatable(content.revision)
  112. yield content
  113. except SameValueError or ValueError as e:
  114. # INFO - 20-03-2018 - renew transaction when error happened
  115. # This avoid bad session data like new "temporary" revision
  116. # to be add when problem happen.
  117. transaction.abort()
  118. transaction.begin()
  119. raise e
  120. finally:
  121. RevisionsIntegrity.remove_from_updatable(content.revision)