瀏覽代碼

Merge pull request #3 from lebouquetin/master

Tracim 10 年之前
父節點
當前提交
9d6012bfc3

+ 1 - 1
tracim/test.ini 查看文件

16
 port = 8080
16
 port = 8080
17
 
17
 
18
 [app:main]
18
 [app:main]
19
-sqlalchemy.url = sqlite:///:memory:
19
+sqlalchemy.url = postgresql://postgres:dummy@127.0.0.1:5432/tracim_test?client_encoding=utf8
20
 use = config:development.ini
20
 use = config:development.ini
21
 
21
 
22
 [app:main_without_authn]
22
 [app:main_without_authn]

+ 4 - 4
tracim/tracim/config/app_cfg.py 查看文件

205
         self.WEBSITE_HOME_TITLE_COLOR = tg.config.get('website.title.color', '#555')
205
         self.WEBSITE_HOME_TITLE_COLOR = tg.config.get('website.title.color', '#555')
206
         self.WEBSITE_HOME_IMAGE_URL = tg.lurl('/assets/img/home_illustration.jpg')
206
         self.WEBSITE_HOME_IMAGE_URL = tg.lurl('/assets/img/home_illustration.jpg')
207
         self.WEBSITE_HOME_BACKGROUND_IMAGE_URL = tg.lurl('/assets/img/bg.jpg')
207
         self.WEBSITE_HOME_BACKGROUND_IMAGE_URL = tg.lurl('/assets/img/bg.jpg')
208
-        self.WEBSITE_BASE_URL = tg.config.get('website.base_url')
208
+        self.WEBSITE_BASE_URL = tg.config.get('website.base_url', '')
209
 
209
 
210
-        self.WEBSITE_HOME_TAG_LINE = tg.config.get('website.home.tag_line')
211
-        self.WEBSITE_SUBTITLE = tg.config.get('website.home.subtitle')
212
-        self.WEBSITE_HOME_BELOW_LOGIN_FORM = tg.config.get('website.home.below_login_form')
210
+        self.WEBSITE_HOME_TAG_LINE = tg.config.get('website.home.tag_line', '')
211
+        self.WEBSITE_SUBTITLE = tg.config.get('website.home.subtitle', '')
212
+        self.WEBSITE_HOME_BELOW_LOGIN_FORM = tg.config.get('website.home.below_login_form', '')
213
 
213
 
214
 
214
 
215
         self.EMAIL_NOTIFICATION_FROM = tg.config.get('email.notification.from')
215
         self.EMAIL_NOTIFICATION_FROM = tg.config.get('email.notification.from')

+ 1 - 1
tracim/tracim/controllers/root.py 查看文件

90
 
90
 
91
 
91
 
92
     @expose()
92
     @expose()
93
-    def post_login(self, came_from=lurl('/')):
93
+    def post_login(self, came_from=lurl('/dashboard')):
94
         """
94
         """
95
         Redirect the user to the initially requested page on successful
95
         Redirect the user to the initially requested page on successful
96
         authentication or redirect her back to the login page if login failed.
96
         authentication or redirect her back to the login page if login failed.

+ 18 - 9
tracim/tracim/lib/content.py 查看文件

5
 import tg
5
 import tg
6
 
6
 
7
 from sqlalchemy.orm.attributes import get_history
7
 from sqlalchemy.orm.attributes import get_history
8
+from sqlalchemy import not_
8
 from tracim.lib import cmp_to_key
9
 from tracim.lib import cmp_to_key
9
-from tracim.lib.notifications import Notifier
10
+from tracim.lib.notifications import NotifierFactory
10
 from tracim.model import DBSession
11
 from tracim.model import DBSession
11
 from tracim.model.auth import User
12
 from tracim.model.auth import User
12
 from tracim.model.data import ContentStatus, ContentRevisionRO, ActionDescription
13
 from tracim.model.data import ContentStatus, ContentRevisionRO, ActionDescription
73
 
74
 
74
     def _base_query(self, workspace: Workspace=None):
75
     def _base_query(self, workspace: Workspace=None):
75
         result = DBSession.query(Content)
76
         result = DBSession.query(Content)
77
+
76
         if workspace:
78
         if workspace:
77
             result = result.filter(Content.workspace_id==workspace.workspace_id)
79
             result = result.filter(Content.workspace_id==workspace.workspace_id)
78
 
80
 
130
         content.revision_type = ActionDescription.CREATION
132
         content.revision_type = ActionDescription.CREATION
131
 
133
 
132
         if do_save:
134
         if do_save:
135
+            DBSession.add(content)
133
             self.save(content, ActionDescription.CREATION)
136
             self.save(content, ActionDescription.CREATION)
134
         return content
137
         return content
135
 
138
 
190
 
193
 
191
     def get_all(self, parent_id: int, content_type: str, workspace: Workspace=None) -> Content:
194
     def get_all(self, parent_id: int, content_type: str, workspace: Workspace=None) -> Content:
192
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
195
         assert parent_id is None or isinstance(parent_id, int) # DYN_REMOVE
193
-        assert content_type is not None # DYN_REMOVE
196
+        assert content_type is not None# DYN_REMOVE
194
         assert isinstance(content_type, str) # DYN_REMOVE
197
         assert isinstance(content_type, str) # DYN_REMOVE
195
 
198
 
196
-        if not parent_id:
197
-            return
199
+        resultset = self._base_query(workspace)
198
 
200
 
199
-        return self._base_query(workspace).\
200
-            filter(Content.parent_id==parent_id).\
201
-            filter(Content.type==content_type).\
202
-            all()
201
+        if content_type!=ContentType.Any:
202
+            resultset.filter(Content.type==content_type)
203
+
204
+        if parent_id:
205
+            resultset.filter(Content.parent_id==parent_id)
206
+
207
+        return resultset.all()
203
 
208
 
204
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
209
     def set_allowed_content(self, folder: Content, allowed_content_dict:dict):
205
         """
210
         """
269
         content.is_deleted = False
274
         content.is_deleted = False
270
         content.revision_type = ActionDescription.UNDELETION
275
         content.revision_type = ActionDescription.UNDELETION
271
 
276
 
277
+    def flush(self):
278
+        DBSession.flush()
279
+
272
     def save(self, content: Content, action_description: str=None, do_flush=True, do_notify=True):
280
     def save(self, content: Content, action_description: str=None, do_flush=True, do_notify=True):
273
         """
281
         """
274
         Save an object, flush the session and set the revision_type property
282
         Save an object, flush the session and set the revision_type property
290
             DBSession.flush()
298
             DBSession.flush()
291
 
299
 
292
         if do_notify:
300
         if do_notify:
293
-            Notifier(self._user).notify_content_update(content)
301
+            NotifierFactory.create(self._user).notify_content_update(content)
302
+

+ 33 - 3
tracim/tracim/lib/notifications.py 查看文件

27
 
27
 
28
 from tgext.asyncjob import asyncjob_perform
28
 from tgext.asyncjob import asyncjob_perform
29
 
29
 
30
-class Notifier(object):
30
+class INotifier(object):
31
+    """
32
+    Interface for Notifier instances
33
+    """
34
+    def __init__(self, current_user: User=None):
35
+        raise NotImplementedError
36
+
37
+    def notify_content_update(self, content: Content):
38
+        raise NotImplementedError
39
+
40
+
41
+class NotifierFactory(object):
42
+
43
+    @classmethod
44
+    def create(cls, current_user: User=None) -> INotifier:
45
+        cfg = CFG.get_instance()
46
+        if cfg.EMAIL_NOTIFICATION_ACTIVATED:
47
+            return DummyNotifier(current_user)
31
 
48
 
49
+        return RealNotifier(current_user)
50
+
51
+
52
+class DummyNotifier(INotifier):
32
     def __init__(self, current_user: User=None):
53
     def __init__(self, current_user: User=None):
33
-        """
54
+        logger.info(self, 'Instantiating Dummy Notifier')
55
+
56
+    def notify_content_update(self, content: Content):
57
+        logger.info(self, 'Fake notifier, do not send email-notification for update of content {} by user {}'.format(content.content_id, self._user.user_id))
34
 
58
 
59
+
60
+class RealNotifier(object):
61
+
62
+    def __init__(self, current_user: User=None):
63
+        """
35
         :param current_user: the user that has triggered the notification
64
         :param current_user: the user that has triggered the notification
36
         :return:
65
         :return:
37
         """
66
         """
67
+        logger.info(self, 'Instantiating Real Notifier')
38
         cfg = CFG.get_instance()
68
         cfg = CFG.get_instance()
39
 
69
 
40
         self._user = current_user
70
         self._user = current_user
44
                                        cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
74
                                        cfg.EMAIL_NOTIFICATION_SMTP_PASSWORD)
45
 
75
 
46
     def notify_content_update(self, content: Content):
76
     def notify_content_update(self, content: Content):
47
-        logger.info(self, 'About to email-notify update of content {} by user {}'.format(content.content_id, self._user.user_id))
77
+        logger.info(self, 'About to email-notify update of content {} by user {}'.format(content.content_id, self._user.user_id if self._user else 0)) # 0 means "no user"
48
 
78
 
49
         global_config = CFG.get_instance()
79
         global_config = CFG.get_instance()
50
 
80
 

+ 17 - 7
tracim/tracim/lib/user.py 查看文件

33
         if do_save:
33
         if do_save:
34
             self.save(user)
34
             self.save(user)
35
 
35
 
36
-        if user.user_id==self._user.user_id:
36
+        if self._user and user.user_id==self._user.user_id:
37
             # this is required for the session to keep on being up-to-date
37
             # this is required for the session to keep on being up-to-date
38
             tg.request.identity['repoze.who.userid'] = email
38
             tg.request.identity['repoze.who.userid'] = email
39
 
39
 
58
 
58
 
59
 class UserStaticApi(object):
59
 class UserStaticApi(object):
60
 
60
 
61
-  @classmethod
62
-  def get_current_user(cls) -> User:
63
-    identity = tg.request.identity
61
+    @classmethod
62
+    def get_current_user(cls) -> User:
63
+        identity = tg.request.identity
64
 
64
 
65
-    if tg.request.identity:
66
-        return pbma.User.by_email_address(tg.request.identity['repoze.who.userid'])
67
-    return None
65
+        if tg.request.identity:
66
+            return cls._get_user(tg.request.identity['repoze.who.userid'])
67
+
68
+        return None
69
+
70
+    @classmethod
71
+    def _get_user(cls, email) -> User:
72
+        """
73
+        Do not use directly in your code.
74
+        :param email:
75
+        :return:
76
+        """
77
+        return pbma.User.by_email_address(email)

+ 12 - 5
tracim/tracim/model/auth.py 查看文件

16
 
16
 
17
 __all__ = ['User', 'Group', 'Permission']
17
 __all__ = ['User', 'Group', 'Permission']
18
 
18
 
19
-from sqlalchemy import Table, ForeignKey, Column
20
-from sqlalchemy.types import Unicode, Integer, DateTime, Boolean
19
+from sqlalchemy import Column
20
+from sqlalchemy import ForeignKey
21
+from sqlalchemy import Sequence
22
+from sqlalchemy import Table
23
+
24
+from sqlalchemy.types import Unicode
25
+from sqlalchemy.types import Integer
26
+from sqlalchemy.types import DateTime
27
+from sqlalchemy.types import Boolean
21
 from sqlalchemy.orm import relation, relationship, synonym
28
 from sqlalchemy.orm import relation, relationship, synonym
22
 
29
 
23
 from tracim.model import DeclarativeBase, metadata, DBSession
30
 from tracim.model import DeclarativeBase, metadata, DBSession
54
 
61
 
55
     __tablename__ = 'groups'
62
     __tablename__ = 'groups'
56
 
63
 
57
-    group_id = Column(Integer, autoincrement=True, primary_key=True)
64
+    group_id = Column(Integer, Sequence('seq__groups__group_id'), autoincrement=True, primary_key=True)
58
     group_name = Column(Unicode(16), unique=True, nullable=False)
65
     group_name = Column(Unicode(16), unique=True, nullable=False)
59
     display_name = Column(Unicode(255))
66
     display_name = Column(Unicode(255))
60
     created = Column(DateTime, default=datetime.now)
67
     created = Column(DateTime, default=datetime.now)
106
     """
113
     """
107
     __tablename__ = 'users'
114
     __tablename__ = 'users'
108
 
115
 
109
-    user_id = Column(Integer, autoincrement=True, primary_key=True)
116
+    user_id = Column(Integer, Sequence('seq__users__user_id'), autoincrement=True, primary_key=True)
110
     email = Column(Unicode(255), unique=True, nullable=False)
117
     email = Column(Unicode(255), unique=True, nullable=False)
111
     display_name = Column(Unicode(255))
118
     display_name = Column(Unicode(255))
112
     _password = Column('password', Unicode(128))
119
     _password = Column('password', Unicode(128))
214
     __tablename__ = 'permissions'
221
     __tablename__ = 'permissions'
215
 
222
 
216
 
223
 
217
-    permission_id = Column(Integer, autoincrement=True, primary_key=True)
224
+    permission_id = Column(Integer, Sequence('seq__permissions__permission_id'), autoincrement=True, primary_key=True)
218
     permission_name = Column(Unicode(63), unique=True, nullable=False)
225
     permission_name = Column(Unicode(63), unique=True, nullable=False)
219
     description = Column(Unicode(255))
226
     description = Column(Unicode(255))
220
 
227
 

+ 4 - 3
tracim/tracim/model/data.py 查看文件

6
 
6
 
7
 from sqlalchemy import Column
7
 from sqlalchemy import Column
8
 from sqlalchemy import ForeignKey
8
 from sqlalchemy import ForeignKey
9
+from sqlalchemy import Sequence
9
 
10
 
10
 from sqlalchemy.ext.hybrid import hybrid_property
11
 from sqlalchemy.ext.hybrid import hybrid_property
11
 
12
 
40
 
41
 
41
     __tablename__ = 'workspaces'
42
     __tablename__ = 'workspaces'
42
 
43
 
43
-    workspace_id = Column(Integer, autoincrement=True, primary_key=True)
44
+    workspace_id = Column(Integer, Sequence('seq__workspaces__workspace_id'), autoincrement=True, primary_key=True)
44
 
45
 
45
     label   = Column(Unicode(1024), unique=False, nullable=False, default='')
46
     label   = Column(Unicode(1024), unique=False, nullable=False, default='')
46
     description = Column(Text(), unique=False, nullable=False, default='')
47
     description = Column(Text(), unique=False, nullable=False, default='')
335
 
336
 
336
     revision_to_serialize = -0  # This flag allow to serialize a given revision if required by the user
337
     revision_to_serialize = -0  # This flag allow to serialize a given revision if required by the user
337
 
338
 
338
-    content_id = Column(Integer, autoincrement=True, primary_key=True)
339
+    content_id = Column(Integer, Sequence('seq__contents__content_id'), autoincrement=True, primary_key=True)
339
     parent_id = Column(Integer, ForeignKey('contents.content_id'), nullable=True, default=None)
340
     parent_id = Column(Integer, ForeignKey('contents.content_id'), nullable=True, default=None)
340
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True, default=None)
341
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True, default=None)
341
 
342
 
475
 
476
 
476
     __tablename__ = 'content_revisions'
477
     __tablename__ = 'content_revisions'
477
 
478
 
478
-    revision_id = Column(Integer, primary_key=True)
479
+    revision_id = Column(Integer, Sequence('seq__content_revisions__revision_id'), primary_key=True)
479
     content_id = Column(Integer, ForeignKey('contents.content_id'))
480
     content_id = Column(Integer, ForeignKey('contents.content_id'))
480
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True)
481
     owner_id = Column(Integer, ForeignKey('users.user_id'), nullable=True)
481
     label = Column(Unicode(1024), unique=False, nullable=False)
482
     label = Column(Unicode(1024), unique=False, nullable=False)

+ 3 - 1
tracim/tracim/templates/index.mak 查看文件

15
                     <p class="text-center" style="color: ${h.CFG.WEBSITE_HOME_TITLE_COLOR};">${h.CFG.WEBSITE_SUBTITLE|n}</p>
15
                     <p class="text-center" style="color: ${h.CFG.WEBSITE_HOME_TITLE_COLOR};">${h.CFG.WEBSITE_SUBTITLE|n}</p>
16
                 </div>
16
                 </div>
17
             </div>
17
             </div>
18
+
18
             <div class="row">
19
             <div class="row">
19
                 <div class="col-sm-offset-3 col-sm-2">
20
                 <div class="col-sm-offset-3 col-sm-2">
20
                     <a class="thumbnail">
21
                     <a class="thumbnail">
27
                 <div class="col-sm-4">
28
                 <div class="col-sm-4">
28
                     <div class="well">
29
                     <div class="well">
29
                         <h2 style="margin-top: 0;">${TIM.ICO(32, 'status/status-locked')} ${_('Login')}</h2>
30
                         <h2 style="margin-top: 0;">${TIM.ICO(32, 'status/status-locked')} ${_('Login')}</h2>
30
-                        <form role="form" method="POST" action="${tg.url('/login_handler', params=dict(came_from=came_from, __logins=login_counter))}">
31
+                        <form id='w-login-form' role="form" method="POST" action="${tg.url('/login_handler', params=dict(came_from=came_from, __logins=login_counter))}">
31
                             <div class="form-group">
32
                             <div class="form-group">
32
                                 <div class="input-group">
33
                                 <div class="input-group">
33
                                     <div class="input-group-addon"><i class="fa fa-envelope-o"></i></div>
34
                                     <div class="input-group-addon"><i class="fa fa-envelope-o"></i></div>
63
                 <div class="col-sm-offset-3 col-sm-6 text-center">${h.CFG.WEBSITE_HOME_BELOW_LOGIN_FORM|n}</div>
64
                 <div class="col-sm-offset-3 col-sm-6 text-center">${h.CFG.WEBSITE_HOME_BELOW_LOGIN_FORM|n}</div>
64
             </div>
65
             </div>
65
 
66
 
67
+
66
         </div>
68
         </div>
67
     </div>
69
     </div>
68
 </div>
70
 </div>

+ 0 - 1
tracim/tracim/templates/master_anonymous.mak 查看文件

23
     background-size: cover;
23
     background-size: cover;
24
     -o-background-size: cover;">
24
     -o-background-size: cover;">
25
         <script src="${tg.url('/assets/js/jquery.min.js')}"></script>
25
         <script src="${tg.url('/assets/js/jquery.min.js')}"></script>
26
-## FIXME - REMOVE        <script src="${tg.url('/javascript/tracim.js')}"></script>
27
 
26
 
28
         <div class="container-fluid">
27
         <div class="container-fluid">
29
             ${self.main_menu()}
28
             ${self.main_menu()}

+ 122 - 5
tracim/tracim/tests/__init__.py 查看文件

10
 from tg import config
10
 from tg import config
11
 from tg.util import Bunch
11
 from tg.util import Bunch
12
 
12
 
13
-from tracim import model
13
+from sqlalchemy.engine import reflection
14
+
15
+from sqlalchemy.schema import DropConstraint
16
+from sqlalchemy.schema import DropSequence
17
+from sqlalchemy.schema import DropTable
18
+from sqlalchemy.schema import ForeignKeyConstraint
19
+from sqlalchemy.schema import MetaData
20
+from sqlalchemy.schema import Sequence
21
+from sqlalchemy.schema import Table
22
+
23
+import transaction
24
+
25
+from tracim.lib.base import logger
14
 
26
 
15
 __all__ = ['setup_app', 'setup_db', 'teardown_db', 'TestController']
27
 __all__ = ['setup_app', 'setup_db', 'teardown_db', 'TestController']
16
 
28
 
24
 
36
 
25
 def setup_app():
37
 def setup_app():
26
     """Setup the application."""
38
     """Setup the application."""
39
+
40
+    engine = config['tg.app_globals'].sa_engine
41
+    inspector = reflection.Inspector.from_engine(engine)
42
+    metadata = MetaData()
43
+
44
+    logger.debug(setup_app, 'Before setup...')
45
+
27
     cmd = SetupAppCommand(Bunch(options=Bunch(verbose_level=1)), Bunch())
46
     cmd = SetupAppCommand(Bunch(options=Bunch(verbose_level=1)), Bunch())
47
+    logger.debug(setup_app, 'After setup, before run...')
48
+
28
     cmd.run(Bunch(config_file='config:test.ini', section_name=None))
49
     cmd.run(Bunch(config_file='config:test.ini', section_name=None))
50
+    logger.debug(setup_app, 'After run...')
51
+
52
+
29
 
53
 
30
 def setup_db():
54
 def setup_db():
31
     """Create the database schema (not needed when you run setup_app)."""
55
     """Create the database schema (not needed when you run setup_app)."""
56
+
32
     engine = config['tg.app_globals'].sa_engine
57
     engine = config['tg.app_globals'].sa_engine
33
-    model.init_model(engine)
34
-    model.metadata.create_all(engine)
58
+    # model.init_model(engine)
59
+    # model.metadata.create_all(engine)
60
+
35
 
61
 
36
 
62
 
37
 def teardown_db():
63
 def teardown_db():
38
     """Destroy the database schema."""
64
     """Destroy the database schema."""
39
     engine = config['tg.app_globals'].sa_engine
65
     engine = config['tg.app_globals'].sa_engine
40
-    model.metadata.drop_all(engine)
66
+    connection = engine.connect()
67
+
68
+    # INFO - D.A. - 2014-12-04
69
+    # Recipe taken from bitbucket:
70
+    # https://bitbucket.org/zzzeek/sqlalchemy/wiki/UsageRecipes/DropEverything
71
+
72
+    inspector = reflection.Inspector.from_engine(engine)
73
+    metadata = MetaData()
74
+
75
+    tbs = []
76
+    all_fks = []
77
+    views = []
78
+
79
+    # INFO - D.A. - 2014-12-04
80
+    # Sequences are hard defined here because SQLA does not allow to reflect them from existing schema
81
+    seqs = [
82
+        Sequence('seq__groups__group_id'),
83
+        Sequence('seq__contents__content_id'),
84
+        Sequence('seq__content_revisions__revision_id'),
85
+        Sequence('seq__permissions__permission_id'),
86
+        Sequence('seq__users__user_id'),
87
+        Sequence('seq__workspaces__workspace_id')
88
+    ]
89
+
90
+    for view_name in inspector.get_view_names():
91
+        v = Table(view_name,metadata)
92
+        views.append(v)
93
+
94
+    for table_name in inspector.get_table_names():
95
+
96
+        fks = []
97
+        for fk in inspector.get_foreign_keys(table_name):
98
+            if not fk['name']:
99
+                continue
100
+            fks.append(
101
+                ForeignKeyConstraint((),(),name=fk['name'])
102
+                )
103
+        t = Table(table_name,metadata,*fks)
104
+        tbs.append(t)
105
+        all_fks.extend(fks)
41
 
106
 
107
+    for fkc in all_fks:
108
+        connection.execute(DropConstraint(fkc))
109
+
110
+    for view in views:
111
+        drop_statement = 'DROP VIEW {}'.format(view.name)
112
+        # engine.execute(drop_statement)
113
+        connection.execute(drop_statement)
114
+
115
+    for table in tbs:
116
+        connection.execute(DropTable(table))
117
+
118
+
119
+    for sequence in seqs:
120
+        try:
121
+            connection.execute(DropSequence(sequence))
122
+        except Exception as e:
123
+            logger.debug(teardown_db, 'Exception while trying to remove sequence {}'.format(sequence.name))
124
+
125
+    transaction.commit()
126
+    engine.dispose()
127
+
128
+
129
+class TestStandard(object):
130
+
131
+    def setUp(self):
132
+        self.app = load_app('main')
133
+
134
+        logger.debug(self, 'Start setUp() by trying to clean database...')
135
+        try:
136
+            teardown_db()
137
+        except Exception as e:
138
+            logger.debug(self, 'teardown() throwed an exception {}'.format(e.__str__()))
139
+        logger.debug(self, 'Start setUp() by trying to clean database... -> done')
140
+
141
+        logger.debug(self, 'Start Application Setup...')
142
+        setup_app()
143
+        logger.debug(self, 'Start Application Setup... -> done')
144
+
145
+        logger.debug(self, 'Start Database Setup...')
146
+        setup_db()
147
+        logger.debug(self, 'Start Database Setup... -> done')
148
+
149
+    def tearDown(self):
150
+        transaction.commit()
42
 
151
 
43
 class TestController(object):
152
 class TestController(object):
44
     """Base functional test case for the controllers.
153
     """Base functional test case for the controllers.
61
     def setUp(self):
170
     def setUp(self):
62
         """Setup test fixture for each functional test method."""
171
         """Setup test fixture for each functional test method."""
63
         self.app = load_app(self.application_under_test)
172
         self.app = load_app(self.application_under_test)
173
+
174
+        try:
175
+            teardown_db()
176
+        except Exception as e:
177
+            print('-> err ({})'.format(e.__str__()))
178
+
64
         setup_app()
179
         setup_app()
180
+        setup_db()
181
+
65
 
182
 
66
     def tearDown(self):
183
     def tearDown(self):
67
         """Tear down test fixture for each functional test method."""
184
         """Tear down test fixture for each functional test method."""
68
-        model.DBSession.remove()
185
+        # model.DBSession.remove()
69
         teardown_db()
186
         teardown_db()

+ 20 - 11
tracim/tracim/tests/functional/test_authentication.py 查看文件

10
 
10
 
11
 from nose.tools import eq_, ok_
11
 from nose.tools import eq_, ok_
12
 
12
 
13
+from tracim.lib.user import UserApi
13
 from tracim.tests import TestController
14
 from tracim.tests import TestController
14
 
15
 
15
 
16
 
23
 
24
 
24
     application_under_test = 'main'
25
     application_under_test = 'main'
25
 
26
 
27
+    login = 'admin@admin.admin'
28
+    password = 'admin@admin.admin'
29
+
26
     def test_forced_login(self):
30
     def test_forced_login(self):
27
         """Anonymous users are forced to login
31
         """Anonymous users are forced to login
28
 
32
 
31
         should be redirected to the initially requested page.
35
         should be redirected to the initially requested page.
32
 
36
 
33
         """
37
         """
38
+
34
         # Requesting a protected area
39
         # Requesting a protected area
35
-        resp = self.app.get('/secc/', status=302)
40
+        resp = self.app.get('/dashboard/', status=302)
36
         ok_( resp.location.startswith('http://localhost/login'))
41
         ok_( resp.location.startswith('http://localhost/login'))
37
         # Getting the login form:
42
         # Getting the login form:
38
         resp = resp.follow(status=200)
43
         resp = resp.follow(status=200)
39
         form = resp.form
44
         form = resp.form
40
         # Submitting the login form:
45
         # Submitting the login form:
41
-        form['login'] = 'manager'
42
-        form['password'] = 'managepass'
46
+        form['login'] = self.login
47
+        form['password'] = self.password
43
         post_login = form.submit(status=302)
48
         post_login = form.submit(status=302)
44
         # Being redirected to the initially requested page:
49
         # Being redirected to the initially requested page:
45
         ok_(post_login.location.startswith('http://localhost/post_login'))
50
         ok_(post_login.location.startswith('http://localhost/post_login'))
46
         initial_page = post_login.follow(status=302)
51
         initial_page = post_login.follow(status=302)
47
         ok_('authtkt' in initial_page.request.cookies,
52
         ok_('authtkt' in initial_page.request.cookies,
48
             "Session cookie wasn't defined: %s" % initial_page.request.cookies)
53
             "Session cookie wasn't defined: %s" % initial_page.request.cookies)
49
-        ok_(initial_page.location.startswith('http://localhost/secc/'),
54
+        ok_(initial_page.location.startswith('http://localhost/dashboard/'),
50
             initial_page.location)
55
             initial_page.location)
51
 
56
 
57
+
52
     def test_voluntary_login(self):
58
     def test_voluntary_login(self):
53
         """Voluntary logins must work correctly"""
59
         """Voluntary logins must work correctly"""
54
         # Going to the login form voluntarily:
60
         # Going to the login form voluntarily:
55
-        resp = self.app.get('/login', status=200)
61
+        resp = self.app.get('/', status=200)
56
         form = resp.form
62
         form = resp.form
57
         # Submitting the login form:
63
         # Submitting the login form:
58
-        form['login'] = 'manager'
59
-        form['password'] = 'managepass'
64
+        form['login'] = self.login
65
+        form['password'] = self.password
60
         post_login = form.submit(status=302)
66
         post_login = form.submit(status=302)
67
+
61
         # Being redirected to the home page:
68
         # Being redirected to the home page:
62
         ok_(post_login.location.startswith('http://localhost/post_login'))
69
         ok_(post_login.location.startswith('http://localhost/post_login'))
70
+        # FIXME - D.A - 2014-12-04 - Why double redirect to post_login ?!
63
         home_page = post_login.follow(status=302)
71
         home_page = post_login.follow(status=302)
64
-        ok_('authtkt' in home_page.request.cookies,
65
-            'Session cookie was not defined: %s' % home_page.request.cookies)
66
-        eq_(home_page.location, 'http://localhost/')
72
+        real_home_page = home_page.follow(status=302)
73
+        ok_('authtkt' in real_home_page.request.cookies,
74
+            'Session cookie was not defined: %s' % real_home_page.request.cookies)
75
+        eq_(real_home_page.location, 'http://localhost/dashboard')
67
 
76
 
68
     def test_logout(self):
77
     def test_logout(self):
69
         """Logouts must work correctly"""
78
         """Logouts must work correctly"""
70
         # Logging in voluntarily the quick way:
79
         # Logging in voluntarily the quick way:
71
-        resp = self.app.get('/login_handler?login=manager&password=managepass',
80
+        resp = self.app.get('/login_handler?login={}&password={}'.format(self.login, self.password),
72
                             status=302)
81
                             status=302)
73
         resp = resp.follow(status=302)
82
         resp = resp.follow(status=302)
74
         ok_('authtkt' in resp.request.cookies,
83
         ok_('authtkt' in resp.request.cookies,

+ 11 - 48
tracim/tracim/tests/functional/test_root.py 查看文件

11
 
11
 
12
 """
12
 """
13
 
13
 
14
+from bs4 import BeautifulSoup
15
+
16
+from nose.tools import eq_
14
 from nose.tools import ok_
17
 from nose.tools import ok_
15
 
18
 
19
+from tracim.lib import helpers as h
16
 from tracim.tests import TestController
20
 from tracim.tests import TestController
17
 
21
 
18
 
22
 
20
     """Tests for the method in the root controller."""
24
     """Tests for the method in the root controller."""
21
 
25
 
22
     def test_index(self):
26
     def test_index(self):
23
-        """The front page is working properly"""
24
         response = self.app.get('/')
27
         response = self.app.get('/')
25
-        msg = 'TurboGears 2 is rapid web application development toolkit '\
26
-              'designed to make your life easier.'
27
-        # You can look for specific strings:
28
-        ok_(msg in response)
29
-
30
-        # You can also access a BeautifulSoup'ed response in your tests
31
-        # (First run $ easy_install BeautifulSoup
32
-        # and then uncomment the next two lines)
33
-
34
-        # links = response.html.findAll('a')
35
-        # print links
36
-        # ok_(links, "Mummy, there are no links here!")
37
-
38
-    def test_environ(self):
39
-        """Displaying the wsgi environ works"""
40
-        response = self.app.get('/environ.html')
41
-        ok_('The keys in the environment are: ' in response)
28
+        eq_(200, response.status_int)
42
 
29
 
43
-    def test_data(self):
44
-        """The data display demo works with HTML"""
45
-        response = self.app.get('/data.html?a=1&b=2')
46
-        expected1 = """<td>a</td>\n                <td>1</td>"""
47
-        expected2 = """<td>b</td>\n                <td>2</td>"""
48
-        body = '\n'.join(response.text.splitlines())
49
-        ok_(expected1 in body, response)
50
-        ok_(expected2 in body, response)
51
-
52
-    def test_data_json(self):
53
-        """The data display demo works with JSON"""
54
-        resp = self.app.get('/data.json?a=1&b=2')
55
-        ok_(dict(page='data', params={'a':'1', 'b':'2'}) == resp.json, resp.json)
56
-
57
-    def test_secc_with_manager(self):
58
-        """The manager can access the secure controller"""
59
-        # Note how authentication is forged:
60
-        environ = {'REMOTE_USER': 'manager'}
61
-        resp = self.app.get('/secc', extra_environ=environ, status=200)
62
-        ok_('Secure Controller here' in resp.text, resp.text)
63
-
64
-    def test_secc_with_editor(self):
65
-        """The editor cannot access the secure controller"""
66
-        environ = {'REMOTE_USER': 'editor'}
67
-        self.app.get('/secc', extra_environ=environ, status=403)
68
-        # It's enough to know that authorization was denied with a 403 status
30
+        msg = 'copyright &copy; 2013 - {} tracim project.'.format(h.current_year())
31
+        ok_(msg in response)
69
 
32
 
70
-    def test_secc_with_anonymous(self):
71
-        """Anonymous users must not access the secure controller"""
72
-        self.app.get('/secc', status=401)
73
-        # It's enough to know that authorization was denied with a 401 status
33
+        forms = BeautifulSoup(response.body).find_all('form')
34
+        print('FORMS = ',forms)
35
+        eq_(1, len(forms))
36
+        eq_('w-login-form', forms[0].get('id'))

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

1
-__author__ = 'damien'
1
+# -*- coding: utf-8 -*-
2
+"""Unit test suite for the models of the application."""

+ 129 - 0
tracim/tracim/tests/library/test_content_api.py 查看文件

1
+# -*- coding: utf-8 -*-
2
+
3
+from nose.tools import eq_
4
+
5
+import transaction
6
+
7
+from tracim.lib.content import compare_content_for_sorting_by_type_and_name
8
+from tracim.lib.content import ContentApi
9
+
10
+from tracim.model.data import Content
11
+from tracim.model.data import ContentType
12
+
13
+from tracim.tests import TestStandard
14
+
15
+
16
+class TestContentApi(TestStandard):
17
+
18
+    def test_compare_content_for_sorting_by_type(self):
19
+        c1 = Content()
20
+        c1.label = ''
21
+        c1.type = 'file'
22
+
23
+        c2 = Content()
24
+        c2.label = ''
25
+        c2.type = 'folder'
26
+
27
+        c11 = c1
28
+
29
+        eq_(1, compare_content_for_sorting_by_type_and_name(c1, c2))
30
+        eq_(-1, compare_content_for_sorting_by_type_and_name(c2, c1))
31
+        eq_(0, compare_content_for_sorting_by_type_and_name(c1, c11))
32
+
33
+    def test_compare_content_for_sorting_by_label(self):
34
+        c1 = Content()
35
+        c1.label = 'bbb'
36
+        c1.type = 'file'
37
+
38
+        c2 = Content()
39
+        c2.label = 'aaa'
40
+        c2.type = 'file'
41
+
42
+        c11 = c1
43
+
44
+        eq_(1, compare_content_for_sorting_by_type_and_name(c1, c2))
45
+        eq_(-1, compare_content_for_sorting_by_type_and_name(c2, c1))
46
+        eq_(0, compare_content_for_sorting_by_type_and_name(c1, c11))
47
+
48
+    def test_sort_by_label_or_filename(self):
49
+        c1 = Content()
50
+        c1.label = 'ABCD'
51
+        c1.type = 'file'
52
+
53
+        c2 = Content()
54
+        c2.label = ''
55
+        c2.type = 'file'
56
+        c2.file_name = 'AABC'
57
+
58
+        c3 = Content()
59
+        c3.label = 'BCDE'
60
+        c3.type = 'file'
61
+
62
+        items = [c1, c2, c3]
63
+        sorteds = ContentApi.sort_content(items)
64
+
65
+        eq_(sorteds[0], c2)
66
+        eq_(sorteds[1], c1)
67
+        eq_(sorteds[2], c3)
68
+
69
+    def test_sort_by_content_type(self):
70
+        c1 = Content()
71
+        c1.label = 'AAAA'
72
+        c1.type = 'file'
73
+
74
+        c2 = Content()
75
+        c2.label = 'BBBB'
76
+        c2.type = 'folder'
77
+
78
+        items = [c1, c2]
79
+        sorteds = ContentApi.sort_content(items)
80
+
81
+        eq_(sorteds[0], c2, 'value is {} instead of {}'.format(sorteds[0].content_id, c2.content_id))
82
+        eq_(sorteds[1], c1, 'value is {} instead of {}'.format(sorteds[1].content_id, c1.content_id))
83
+
84
+class TestContentApiFilteringDeletedItem(TestStandard):
85
+
86
+    def test_delete(self):
87
+        api = ContentApi(None)
88
+        item = api.create(ContentType.Folder, None, None, 'not_deleted', True)
89
+        item2 = api.create(ContentType.Folder, None, None, 'to_delete', True)
90
+        transaction.commit()
91
+
92
+        items = api.get_all(None, ContentType.Any, None)
93
+        eq_(2, len(items))
94
+
95
+        items = api.get_all(None, ContentType.Any, None)
96
+        api.delete(items[0])
97
+        transaction.commit()
98
+
99
+        items = api.get_all(None, ContentType.Any, None)
100
+        eq_(1, len(items))
101
+        transaction.commit()
102
+
103
+        # Test that the item is still available if "show deleted" is activated
104
+        api = ContentApi(None, show_deleted=True)
105
+        items = api.get_all(None, ContentType.Any, None)
106
+        eq_(2, len(items))
107
+
108
+
109
+    def test_archive(self):
110
+        api = ContentApi(None)
111
+        item = api.create(ContentType.Folder, None, None, 'not_archived', True)
112
+        item2 = api.create(ContentType.Folder, None, None, 'to_archive', True)
113
+        transaction.commit()
114
+
115
+        items = api.get_all(None, ContentType.Any, None)
116
+        eq_(2, len(items))
117
+
118
+        items = api.get_all(None, ContentType.Any, None)
119
+        api.archive(items[0])
120
+        transaction.commit()
121
+
122
+        items = api.get_all(None, ContentType.Any, None)
123
+        eq_(1, len(items))
124
+        transaction.commit()
125
+
126
+        # Test that the item is still available if "show deleted" is activated
127
+        api = ContentApi(None, show_archived=True)
128
+        items = api.get_all(None, ContentType.Any, None)
129
+        eq_(2, len(items))

+ 91 - 0
tracim/tracim/tests/library/test_serializers.py 查看文件

1
+# -*- coding: utf-8 -*-
2
+
3
+from nose.tools import eq_
4
+from nose.tools import ok_
5
+from nose.tools import raises
6
+
7
+from sqlalchemy.orm.exc import NoResultFound
8
+
9
+import transaction
10
+
11
+
12
+from tracim.model.data import Content
13
+
14
+from tracim.model.serializers import Context
15
+from tracim.model.serializers import ContextConverterNotFoundException
16
+from tracim.model.serializers import CTX
17
+from tracim.model.serializers import DictLikeClass
18
+
19
+from tracim.model.data import ActionDescription
20
+
21
+from tracim.lib.user import UserApi
22
+
23
+from tracim.tests import TestStandard
24
+
25
+
26
+
27
+class TestSerializers(TestStandard):
28
+
29
+    def test_DictLikeClass(self):
30
+        instance = DictLikeClass()
31
+
32
+        instance.bob = 'titi'
33
+        ok_(instance.bob==instance['bob'])
34
+
35
+        instance['titi'] = 'bob'
36
+        ok_(instance.titi==instance['titi'])
37
+
38
+        instance2 = DictLikeClass(instance)
39
+        ok_(instance2.bob==instance2['bob'])
40
+        ok_(instance2.bob==instance.bob)
41
+        ok_(instance2.titi==instance2['titi'])
42
+        ok_(instance2.titi==instance.titi)
43
+
44
+        instance3 = DictLikeClass({'bob': 'titi', 'toto': 'bib'})
45
+        ok_(instance3.bob=='titi')
46
+        ok_(instance3.bob==instance3['bob'])
47
+
48
+        ok_(instance3.toto=='bib')
49
+        ok_(instance3.toto==instance3['toto'])
50
+
51
+    def test_ContextConverterNotFoundException(self):
52
+        class DummyClass(object):
53
+            pass
54
+        context = 'some_context'
55
+        e = ContextConverterNotFoundException(context, DummyClass)
56
+        eq_('converter not found (context: some_context - model: DummyClass)', e.__str__())
57
+
58
+    def test_serialize_ActionDescription_DEFAULT(self):
59
+        obj = ActionDescription('archiving')
60
+        obj.icon = 'places/folder-remote'
61
+        obj.label = 'edit the content'
62
+
63
+        res = Context(CTX.DEFAULT).toDict(obj)
64
+        eq_(res.__class__, DictLikeClass)
65
+        eq_(obj.id, res.id)
66
+        eq_(obj.label, res.label)
67
+        eq_(obj.icon, res.icon)
68
+        eq_(3, len(res.keys()))
69
+
70
+    def test_serialize_Content_DEFAULT(self):
71
+        obj = Content()
72
+        obj.content_id = 132
73
+        obj.label = 'Some label'
74
+        obj.description = 'Some Description'
75
+
76
+        res = Context(CTX.DEFAULT).toDict(obj)
77
+        eq_(res.__class__, DictLikeClass, res)
78
+        eq_(obj.content_id, res.id, res)
79
+        eq_(obj.label, res.label, res)
80
+
81
+        ok_('folder' in res.keys())
82
+        ok_('id' in res.folder.keys())
83
+        eq_(None, res.folder.id)
84
+        eq_(1, len(res.folder.keys()))
85
+
86
+        ok_('workspace' in res.keys())
87
+        eq_(None, res.workspace, res)
88
+        eq_(4, len(res.keys()), res)
89
+
90
+
91
+

+ 66 - 0
tracim/tracim/tests/library/test_user_api.py 查看文件

1
+# -*- coding: utf-8 -*-
2
+
3
+from nose.tools import eq_
4
+from nose.tools import ok_
5
+from nose.tools import raises
6
+
7
+from sqlalchemy.orm.exc import NoResultFound
8
+
9
+import transaction
10
+
11
+from tracim.lib.user import UserApi
12
+from tracim.tests import TestStandard
13
+
14
+
15
+
16
+class TestUserApi(TestStandard):
17
+
18
+    def test_create_and_update_user(self):
19
+        api = UserApi(None)
20
+        u = api.create_user()
21
+        api.update(u, 'bob', 'bob@bob', True)
22
+
23
+        nu = api.get_one_by_email('bob@bob')
24
+        ok_(nu!=None)
25
+        eq_('bob@bob', nu.email)
26
+        eq_('bob', nu.display_name)
27
+
28
+
29
+    def test_user_with_email_exists(self):
30
+        api = UserApi(None)
31
+        u = api.create_user()
32
+        api.update(u, 'bibi', 'bibi@bibi', True)
33
+        transaction.commit()
34
+
35
+        eq_(True, api.user_with_email_exists('bibi@bibi'))
36
+        eq_(False, api.user_with_email_exists('unknown'))
37
+
38
+
39
+    def test_get_one_by_email(self):
40
+        api = UserApi(None)
41
+        u = api.create_user()
42
+        api.update(u, 'bibi', 'bibi@bibi', True)
43
+        uid = u.user_id
44
+        transaction.commit()
45
+
46
+        eq_(uid, api.get_one_by_email('bibi@bibi').user_id)
47
+
48
+    @raises(NoResultFound)
49
+    def test_get_one_by_email_exception(self):
50
+        api = UserApi(None)
51
+        api.get_one_by_email('unknown')
52
+
53
+    def test_get_all(self):
54
+        api = UserApi(None)
55
+        # u1 = api.create_user(True)
56
+        # u2 = api.create_user(True)
57
+
58
+        # users = api.get_all()
59
+        # ok_(2==len(users))
60
+
61
+    def test_get_one(self):
62
+        api = UserApi(None)
63
+        u = api.create_user()
64
+        api.update(u, 'titi', 'titi@titi', True)
65
+        one = api.get_one(u.user_id)
66
+        eq_(u.user_id, one.user_id)

+ 0 - 62
tracim/tracim/tests/models/__init__.py 查看文件

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 """Unit test suite for the models of the application."""
2
 """Unit test suite for the models of the application."""
3
-
4
-from nose.tools import eq_
5
-from tracim.model import DBSession
6
-from tracim.tests import load_app
7
-from tracim.tests import setup_db, teardown_db
8
-
9
-__all__ = ['ModelTest']
10
-
11
-
12
-def setup():
13
-    """Setup test fixture for all model tests."""
14
-    load_app()
15
-    setup_db()
16
-
17
-
18
-def teardown():
19
-    """Tear down test fixture for all model tests."""
20
-    teardown_db()
21
-
22
-
23
-class ModelTest(object):
24
-    """Base unit test case for the models."""
25
-
26
-    klass = None
27
-    attrs = {}
28
-
29
-    def setUp(self):
30
-        """Setup test fixture for each model test method."""
31
-        try:
32
-            new_attrs = {}
33
-            new_attrs.update(self.attrs)
34
-            new_attrs.update(self.do_get_dependencies())
35
-            self.obj = self.klass(**new_attrs)
36
-            DBSession.add(self.obj)
37
-            DBSession.flush()
38
-            return self.obj
39
-        except:
40
-            DBSession.rollback()
41
-            raise
42
-
43
-    def tearDown(self):
44
-        """Tear down test fixture for each model test method."""
45
-        DBSession.rollback()
46
-
47
-    def do_get_dependencies(self):
48
-        """Get model test dependencies.
49
-
50
-        Use this method to pull in other objects that need to be created
51
-        for this object to be build properly.
52
-
53
-        """
54
-        return {}
55
-
56
-    def test_create_obj(self):
57
-        """Model objects can be created"""
58
-        pass
59
-
60
-    def test_query_obj(self):
61
-        """Model objects can be queried"""
62
-        obj = DBSession.query(self.klass).one()
63
-        for key, value in self.attrs.items():
64
-            eq_(getattr(obj, key), value)

+ 0 - 52
tracim/tracim/tests/models/test_auth.py 查看文件

1
-# -*- coding: utf-8 -*-
2
-"""Test suite for the TG app's models"""
3
-from __future__ import unicode_literals
4
-from nose.tools import eq_
5
-
6
-from tracim import model
7
-from tracim.tests.models import ModelTest
8
-
9
-class TestGroup(ModelTest):
10
-    """Unit test case for the ``Group`` model."""
11
-    klass = model.Group
12
-    attrs = dict(
13
-        group_name = "test_group",
14
-        display_name = "Test Group"
15
-        )
16
-
17
-
18
-class TestUser(ModelTest):
19
-    """Unit test case for the ``User`` model."""
20
-    
21
-    klass = model.User
22
-    attrs = dict(
23
-        user_name = "ignucius",
24
-        email = "ignucius@example.org"
25
-        )
26
-
27
-    def test_obj_creation_username(self):
28
-        """The obj constructor must set the user name right"""
29
-        eq_(self.obj.user_name, "ignucius")
30
-
31
-    def test_obj_creation_email(self):
32
-        """The obj constructor must set the email right"""
33
-        eq_(self.obj.email, "ignucius@example.org")
34
-
35
-    def test_no_permissions_by_default(self):
36
-        """User objects should have no permission by default."""
37
-        eq_(len(self.obj.permissions), 0)
38
-
39
-    def test_getting_by_email(self):
40
-        """Users should be fetcheable by their email addresses"""
41
-        him = model.User.by_email_address("ignucius@example.org")
42
-        eq_(him, self.obj)
43
-
44
-
45
-class TestPermission(ModelTest):
46
-    """Unit test case for the ``Permission`` model."""
47
-    
48
-    klass = model.Permission
49
-    attrs = dict(
50
-        permission_name = "test_permission",
51
-        description = "This is a test Description"
52
-        )

+ 34 - 0
tracim/tracim/tests/models/test_user.py 查看文件

1
+import transaction
2
+
3
+from nose.tools import eq_
4
+from nose.tools import ok_
5
+
6
+from tracim.model import DBSession
7
+
8
+from tracim.tests import TestStandard
9
+
10
+
11
+from tracim.model.auth import User
12
+
13
+
14
+class TestUserModel(TestStandard):
15
+
16
+    def test_create(self):
17
+        DBSession.flush()
18
+        transaction.commit()
19
+        name = 'Damien'
20
+        email = 'damien@accorsi.info'
21
+
22
+        user = User()
23
+        user.display_name = name
24
+        user.email = email
25
+
26
+        DBSession.add(user)
27
+        DBSession.flush()
28
+        transaction.commit()
29
+
30
+        new_user = DBSession.query(User).filter(User.display_name==name).one()
31
+
32
+        eq_(new_user.display_name, name)
33
+        eq_(new_user.email, email)
34
+        eq_(new_user.email_address, email)

+ 4 - 3
tracim/tracim/websetup/schema.py 查看文件

48
 -- COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
48
 -- COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
49
 SET search_path = public, pg_catalog;
49
 SET search_path = public, pg_catalog;
50
 
50
 
51
-CREATE FUNCTION update_node() RETURNS trigger
51
+
52
+CREATE OR REPLACE FUNCTION update_node() RETURNS trigger
52
     LANGUAGE plpgsql
53
     LANGUAGE plpgsql
53
     AS $$
54
     AS $$
54
 BEGIN
55
 BEGIN
60
 END;
61
 END;
61
 $$;
62
 $$;
62
 
63
 
63
-CREATE FUNCTION set_created() RETURNS trigger
64
+CREATE OR REPLACE FUNCTION set_created() RETURNS trigger
64
     LANGUAGE plpgsql
65
     LANGUAGE plpgsql
65
     AS $$
66
     AS $$
66
 BEGIN
67
 BEGIN
70
 END;
71
 END;
71
 $$;
72
 $$;
72
 
73
 
73
-CREATE FUNCTION set_updated() RETURNS trigger
74
+CREATE OR REPLACE FUNCTION set_updated() RETURNS trigger
74
     LANGUAGE plpgsql
75
     LANGUAGE plpgsql
75
     AS $$
76
     AS $$
76
 BEGIN
77
 BEGIN