瀏覽代碼

Model integration into tracim v2 (uncomplete)

Guénaël Muller 6 年之前
父節點
當前提交
a000568f5c

+ 0 - 2
.gitignore 查看文件

@@ -64,5 +64,3 @@ wsgidav.conf
64 64
 # binary translation files
65 65
 *.mo
66 66
 
67
-# Virtualenv
68
-/venv/

+ 1 - 1
tracim/__init__.py 查看文件

@@ -1,6 +1,6 @@
1 1
 from pyramid.config import Configurator
2 2
 
3
-from config import RequestWithCFG
3
+from tracim.config import RequestWithCFG
4 4
 
5 5
 
6 6
 def main(global_config, **settings):

+ 57 - 0
tracim/exceptions.py 查看文件

@@ -0,0 +1,57 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+
4
+class TracimError(Exception):
5
+    pass
6
+
7
+class TracimException(Exception):
8
+    pass
9
+
10
+class RunTimeError(TracimError):
11
+    pass
12
+
13
+
14
+class ContentRevisionUpdateError(RuntimeError):
15
+    pass
16
+
17
+
18
+class ContentRevisionDeleteError(ContentRevisionUpdateError):
19
+    pass
20
+
21
+
22
+class ConfigurationError(TracimError):
23
+    pass
24
+
25
+
26
+class AlreadyExistError(TracimError):
27
+    pass
28
+
29
+
30
+class CommandError(TracimError):
31
+    pass
32
+
33
+
34
+class CommandAbortedError(CommandError):
35
+    pass
36
+
37
+class DaemonException(TracimException):
38
+    pass
39
+
40
+
41
+class AlreadyRunningDaemon(DaemonException):
42
+    pass
43
+
44
+
45
+class CalendarException(TracimException):
46
+    pass
47
+
48
+
49
+class UnknownCalendarType(CalendarException):
50
+    pass
51
+
52
+
53
+class NotFound(TracimException):
54
+    pass
55
+
56
+class SameValueError(ValueError):
57
+    pass

+ 6 - 1
tracim/models/__init__.py 查看文件

@@ -1,11 +1,15 @@
1 1
 from sqlalchemy import engine_from_config
2
+from sqlalchemy.event import listen
2 3
 from sqlalchemy.orm import sessionmaker
3 4
 from sqlalchemy.orm import configure_mappers
4 5
 import zope.sqlalchemy
5
-
6
+from .meta import DeclarativeBase
7
+from .revision_protection import prevent_content_revision_delete
6 8
 # import or define all models here to ensure they are attached to the
7 9
 # Base.metadata prior to any initialization routines
8 10
 from .mymodel import MyModel  # flake8: noqa
11
+from .auth import User, Group, Permission
12
+from .data import Content, ContentRevisionRO
9 13
 
10 14
 # run configure_mappers after defining all of the models to ensure
11 15
 # all relationships can be setup
@@ -46,6 +50,7 @@ def get_tm_session(session_factory, transaction_manager):
46 50
     dbsession = session_factory()
47 51
     zope.sqlalchemy.register(
48 52
         dbsession, transaction_manager=transaction_manager)
53
+    listen(dbsession, 'before_flush', prevent_content_revision_delete)
49 54
     return dbsession
50 55
 
51 56
 

+ 21 - 24
tracim/models/auth.py 查看文件

@@ -29,13 +29,12 @@ from sqlalchemy.types import DateTime
29 29
 from sqlalchemy.types import Integer
30 30
 from sqlalchemy.types import Unicode
31 31
 
32
-from tracim.lib.utils import lazy_ugettext as l_
33
-from tracim.model import DBSession
34
-from tracim.model import DeclarativeBase
35
-from tracim.model import metadata
32
+from tracim.translation import fake_translator as l_
33
+from tracim.models.meta import DeclarativeBase
34
+from tracim.models.meta import metadata
36 35
 if TYPE_CHECKING:
37
-    from tracim.model.data import Workspace
38
-
36
+    from tracim.models.data import Workspace
37
+    from tracim.models.data import UserRoleInWorkspace
39 38
 __all__ = ['User', 'Group', 'Permission']
40 39
 
41 40
 # This is the association table for the many-to-many relationship between
@@ -85,9 +84,9 @@ class Group(DeclarativeBase):
85 84
         return self.group_name
86 85
 
87 86
     @classmethod
88
-    def by_group_name(cls, group_name):
87
+    def by_group_name(cls, group_name, dbsession):
89 88
         """Return the user object whose email address is ``email``."""
90
-        return DBSession.query(cls).filter_by(group_name=group_name).first()
89
+        return dbsession.query(cls).filter_by(group_name=group_name).first()
91 90
 
92 91
 
93 92
 class Profile(object):
@@ -157,23 +156,24 @@ class User(DeclarativeBase):
157 156
             profile_id = max(group.group_id for group in self.groups)
158 157
         return Profile(profile_id)
159 158
 
160
-    @property
161
-    def calendar_url(self) -> str:
162
-        # TODO - 20160531 - Bastien: Cyclic import if import in top of file
163
-        from tracim.lib.calendar import CalendarManager
164
-        calendar_manager = CalendarManager(None)
165
-
166
-        return calendar_manager.get_user_calendar_url(self.user_id)
159
+    # TODO - G-M - 27-03-2018 - Check about calendar code
160
+    # @property
161
+    # def calendar_url(self) -> str:
162
+    #     # TODO - 20160531 - Bastien: Cyclic import if import in top of file
163
+    #     from tracim.lib.calendar import CalendarManager
164
+    #     calendar_manager = CalendarManager(None)
165
+    #
166
+    #     return calendar_manager.get_user_calendar_url(self.user_id)
167 167
 
168 168
     @classmethod
169
-    def by_email_address(cls, email):
169
+    def by_email_address(cls, email, dbsession):
170 170
         """Return the user object whose email address is ``email``."""
171
-        return DBSession.query(cls).filter_by(email=email).first()
171
+        return dbsession.query(cls).filter_by(email=email).first()
172 172
 
173 173
     @classmethod
174
-    def by_user_name(cls, username):
174
+    def by_user_name(cls, username, dbsession):
175 175
         """Return the user object whose user name is ``username``."""
176
-        return DBSession.query(cls).filter_by(email=username).first()
176
+        return dbsession.query(cls).filter_by(email=username).first()
177 177
 
178 178
     @classmethod
179 179
     def _hash_password(cls, cleartext_password: str) -> str:
@@ -252,7 +252,6 @@ class User(DeclarativeBase):
252 252
             if role.workspace == workspace:
253 253
                 return role.role
254 254
 
255
-        from tracim.model.data import UserRoleInWorkspace
256 255
         return UserRoleInWorkspace.NOT_APPLICABLE
257 256
 
258 257
     def get_active_roles(self) -> ['UserRoleInWorkspace']:
@@ -265,20 +264,18 @@ class User(DeclarativeBase):
265 264
                 roles.append(role)
266 265
         return roles
267 266
 
268
-    def ensure_auth_token(self) -> None:
267
+    def ensure_auth_token(self, validity_seconds, dbsession) -> None:
269 268
         """
270 269
         Create auth_token if None, regenerate auth_token if too much old.
271 270
 
272 271
         auth_token validity is set in
273 272
         :return:
274 273
         """
275
-        from tracim.config.app_cfg import CFG
276
-        validity_seconds = CFG.get_instance().USER_AUTH_TOKEN_VALIDITY
277 274
 
278 275
         if not self.auth_token or not self.auth_token_created:
279 276
             self.auth_token = str(uuid.uuid4())
280 277
             self.auth_token_created = datetime.utcnow()
281
-            DBSession.flush()
278
+            dbsession.flush()
282 279
             return
283 280
 
284 281
         now_seconds = time.mktime(datetime.utcnow().timetuple())

+ 13 - 12
tracim/models/data.py 查看文件

@@ -5,7 +5,6 @@ import json
5 5
 import os
6 6
 from datetime import datetime
7 7
 
8
-import tg
9 8
 from babel.dates import format_timedelta
10 9
 from bs4 import BeautifulSoup
11 10
 from sqlalchemy import Column, inspect, Index
@@ -28,10 +27,10 @@ from depot.fields.sqlalchemy import UploadedFileField
28 27
 from depot.fields.upload import UploadedFile
29 28
 from depot.io.utils import FileIntent
30 29
 
31
-from tracim.lib.utils import lazy_ugettext as l_
32
-from tracim.lib.exception import ContentRevisionUpdateError
33
-from tracim.model import DeclarativeBase, RevisionsIntegrity
34
-from tracim.model.auth import User
30
+from tracim.translation import fake_translator as l_
31
+from tracim.exceptions import ContentRevisionUpdateError
32
+from tracim.models.meta import DeclarativeBase
33
+from tracim.models.auth import User
35 34
 
36 35
 DEFAULT_PROPERTIES = dict(
37 36
     allowed_content=dict(
@@ -86,13 +85,14 @@ class Workspace(DeclarativeBase):
86 85
 
87 86
         return contents
88 87
 
89
-    @property
90
-    def calendar_url(self) -> str:
91
-        # TODO - 20160531 - Bastien: Cyclic import if import in top of file
92
-        from tracim.lib.calendar import CalendarManager
93
-        calendar_manager = CalendarManager(None)
94
-
95
-        return calendar_manager.get_workspace_calendar_url(self.workspace_id)
88
+    # TODO - G-M - 27-03-2018 - Check about calendar code
89
+    # @property
90
+    # def calendar_url(self) -> str:
91
+    #     # TODO - 20160531 - Bastien: Cyclic import if import in top of file
92
+    #     from tracim.lib.calendar import CalendarManager
93
+    #     calendar_manager = CalendarManager(None)
94
+    #
95
+    #     return calendar_manager.get_workspace_calendar_url(self.workspace_id)
96 96
 
97 97
     def get_user_role(self, user: User) -> int:
98 98
         for role in user.roles:
@@ -522,6 +522,7 @@ class ContentChecker(object):
522 522
         # TODO - G.M - 15-03-2018 - Choose only correct Content-type for origin
523 523
         # Only content who can be copied need this
524 524
         if item.type == ContentType.Any:
525
+            properties = item.properties
525 526
             if 'origin' in properties.keys():
526 527
                 return True
527 528
         raise NotImplementedError

+ 4 - 1
tracim/models/meta.py 查看文件

@@ -1,5 +1,8 @@
1 1
 from sqlalchemy.ext.declarative import declarative_base
2 2
 from sqlalchemy.schema import MetaData
3
+from sqlalchemy import inspect
4
+
5
+from tracim.exceptions import ContentRevisionUpdateError
3 6
 
4 7
 # Recommended naming convention used by Alembic, as various different database
5 8
 # providers will autogenerate vastly different names making migrations more
@@ -13,4 +16,4 @@ NAMING_CONVENTION = {
13 16
 }
14 17
 
15 18
 metadata = MetaData(naming_convention=NAMING_CONVENTION)
16
-Base = declarative_base(metadata=metadata)
19
+DeclarativeBase = declarative_base(metadata=metadata)

+ 2 - 2
tracim/models/mymodel.py 查看文件

@@ -5,10 +5,10 @@ from sqlalchemy import (
5 5
     Text,
6 6
 )
7 7
 
8
-from .meta import Base
8
+from .meta import DeclarativeBase
9 9
 
10 10
 
11
-class MyModel(Base):
11
+class MyModel(DeclarativeBase):
12 12
     __tablename__ = 'models'
13 13
     id = Column(Integer, primary_key=True)
14 14
     name = Column(Text)

+ 2 - 2
tracim/models/organisational.py 查看文件

@@ -1,5 +1,5 @@
1
-from tracim.model import User
2
-from tracim.model.data import UserRoleInWorkspace
1
+from tracim.models import User
2
+from tracim.models.data import UserRoleInWorkspace
3 3
 
4 4
 
5 5
 CALENDAR_PERMISSION_READ = 'r'

+ 96 - 0
tracim/models/revision_protection.py 查看文件

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

+ 2 - 0
tracim/routes.py 查看文件

@@ -1,3 +1,5 @@
1 1
 def includeme(config):
2 2
     config.add_static_view('static', 'static', cache_max_age=3600)
3 3
     config.add_route('home', '/')
4
+    config.add_route('test_config', '/test_config')
5
+    config.add_route('test_model', '/test_model')

+ 2 - 2
tracim/scripts/initializedb.py 查看文件

@@ -9,7 +9,7 @@ from pyramid.paster import (
9 9
 
10 10
 from pyramid.scripts.common import parse_vars
11 11
 
12
-from ..models.meta import Base
12
+from ..models.meta import DeclarativeBase
13 13
 from ..models import (
14 14
     get_engine,
15 15
     get_session_factory,
@@ -34,7 +34,7 @@ def main(argv=sys.argv):
34 34
     settings = get_appsettings(config_uri, options=options)
35 35
 
36 36
     engine = get_engine(settings)
37
-    Base.metadata.create_all(engine)
37
+    DeclarativeBase.metadata.create_all(engine)
38 38
 
39 39
     session_factory = get_session_factory(engine)
40 40
 

+ 1 - 1
tracim/templates/mytemplate.jinja2 查看文件

@@ -3,6 +3,6 @@
3 3
 {% block content %}
4 4
 <div class="content">
5 5
   <h1><span class="font-semi-bold">Pyramid</span> <span class="smaller">Alchemy project</span></h1>
6
-  <p class="lead">Welcome to <span class="font-normal">tracim</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
6
+  <p class="lead">Welcome to <span class="font-normal">{{ project }}</span>, a&nbsp;Pyramid application generated&nbsp;by<br><span class="font-normal">Cookiecutter</span>.</p>
7 7
 </div>
8 8
 {% endblock content %}

+ 4 - 0
tracim/translation.py 查看文件

@@ -0,0 +1,4 @@
1
+
2
+# TODO - G.M - 27-03-2018 - Reconnect true internationalization
3
+def fake_translator(text: str):
4
+    return text

+ 18 - 1
tracim/views/default.py 查看文件

@@ -4,11 +4,28 @@ from pyramid.view import view_config
4 4
 from sqlalchemy.exc import DBAPIError
5 5
 
6 6
 from ..models import MyModel
7
+from ..models import Content
8
+
9
+
10
+@view_config(route_name='test_config', renderer='../templates/mytemplate.jinja2')
11
+def test_config(request):
12
+    try:
13
+        project = request.config().WEBSITE_TITLE
14
+    except Exception as e:
15
+        return Response(e, content_type='text/plain', status=500)
16
+    return {'project': project}
17
+
18
+@view_config(route_name='test_model', renderer='../templates/mytemplate.jinja2')
19
+def test_model(request):
20
+    try:
21
+        # project = request.dbsession.query(MyModel)
22
+    except Exception as e:
23
+        return Response(e, content_type='text/plain', status=500)
24
+    return {'project': project}
7 25
 
8 26
 
9 27
 @view_config(route_name='home', renderer='../templates/mytemplate.jinja2')
10 28
 def my_view(request):
11
-    request.config()
12 29
     try:
13 30
         query = request.dbsession.query(MyModel)
14 31
         one = query.filter(MyModel.name == 'one').first()