Procházet zdrojové kódy

merged master into 85/dev/beta_1.0/archived_deleted_shown

Come před 7 roky
rodič
revize
7302218264

+ 2 - 1
README.md Zobrazit soubor

318
 The main parameters for notifications are the following ones:
318
 The main parameters for notifications are the following ones:
319
 
319
 
320
     email.notification.activated = true
320
     email.notification.activated = true
321
-    email.notification.from = Tracim Notification <tracim@tmycompany.com>
321
+    email.notification.from.email = noreply@trac.im
322
+    email.notification.from.default_label = Tracim Notification
322
     email.notification.smtp.server = smtp.mycompany.com
323
     email.notification.smtp.server = smtp.mycompany.com
323
     email.notification.smtp.port = 25
324
     email.notification.smtp.port = 25
324
     email.notification.smtp.user = username
325
     email.notification.smtp.user = username

+ 1 - 1
install/requirements.txt Zobrazit soubor

51
 tgext.admin==0.6.4
51
 tgext.admin==0.6.4
52
 tgext.asyncjob==0.3.1
52
 tgext.asyncjob==0.3.1
53
 tgext.crud==0.7.3
53
 tgext.crud==0.7.3
54
-tgext.pluggable==0.5.5
54
+tgext.pluggable==0.6.2
55
 transaction==1.4.4
55
 transaction==1.4.4
56
 tw2.core==2.2.2
56
 tw2.core==2.2.2
57
 tw2.forms==2.2.2.1
57
 tw2.forms==2.2.2.1

+ 2 - 1
tracim/development.ini.base Zobrazit soubor

176
 website.server_name = 127.0.0.1
176
 website.server_name = 127.0.0.1
177
     
177
     
178
 email.notification.activated = False
178
 email.notification.activated = False
179
-email.notification.from = Tracim Notification <noreply@trac.im>
179
+email.notification.from.email = noreply@trac.im
180
+email.notification.from.default_label = Tracim Notifications
180
 email.notification.content_update.template.html = ./tracim/templates/mail/content_update_body_html.mak
181
 email.notification.content_update.template.html = ./tracim/templates/mail/content_update_body_html.mak
181
 email.notification.content_update.template.text = ./tracim/templates/mail/content_update_body_text.mak
182
 email.notification.content_update.template.text = ./tracim/templates/mail/content_update_body_text.mak
182
 email.notification.created_account.template.html = ./tracim/templates/mail/created_account_body_html.mak
183
 email.notification.created_account.template.html = ./tracim/templates/mail/created_account_body_html.mak

+ 1 - 0
tracim/migration/env.py Zobrazit soubor

65
             context.run_migrations()
65
             context.run_migrations()
66
     finally:
66
     finally:
67
         connection.close()
67
         connection.close()
68
+        engine.dispose()
68
 
69
 
69
 if context.is_offline_mode():
70
 if context.is_offline_mode():
70
     run_migrations_offline()
71
     run_migrations_offline()

+ 13 - 1
tracim/tracim/config/app_cfg.py Zobrazit soubor

202
         self.WEBSITE_SUBTITLE = tg.config.get('website.home.subtitle', '')
202
         self.WEBSITE_SUBTITLE = tg.config.get('website.home.subtitle', '')
203
         self.WEBSITE_HOME_BELOW_LOGIN_FORM = tg.config.get('website.home.below_login_form', '')
203
         self.WEBSITE_HOME_BELOW_LOGIN_FORM = tg.config.get('website.home.below_login_form', '')
204
 
204
 
205
+        if tg.config.get('email.notification.from'):
206
+            raise Exception(
207
+                'email.notification.from configuration is deprecated. '
208
+                'Use instead email.notification.from.email and '
209
+                'email.notification.from.default_label.'
210
+            )
205
 
211
 
206
-        self.EMAIL_NOTIFICATION_FROM = tg.config.get('email.notification.from')
212
+        self.EMAIL_NOTIFICATION_FROM_EMAIL = \
213
+            tg.config.get('email.notification.from.email')
214
+        self.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL = \
215
+            tg.config.get('email.notification.from.default_label')
207
         self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = tg.config.get('email.notification.content_update.template.html')
216
         self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = tg.config.get('email.notification.content_update.template.html')
208
         self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = tg.config.get('email.notification.content_update.template.text')
217
         self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = tg.config.get('email.notification.content_update.template.text')
209
         self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = tg.config.get(
218
         self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = tg.config.get(
327
                 )
336
                 )
328
             )
337
             )
329
 
338
 
339
+        if not self.WSGIDAV_CLIENT_BASE_URL.endswith('/'):
340
+            self.WSGIDAV_CLIENT_BASE_URL += '/'
341
+
330
 
342
 
331
     def get_tracker_js_content(self, js_tracker_file_path = None):
343
     def get_tracker_js_content(self, js_tracker_file_path = None):
332
         js_tracker_file_path = tg.config.get('js_tracker_path', None)
344
         js_tracker_file_path = tg.config.get('js_tracker_path', None)

+ 6 - 2
tracim/tracim/config/middleware.py Zobrazit soubor

3
 
3
 
4
 from tracim.config.app_cfg import base_config
4
 from tracim.config.app_cfg import base_config
5
 from tracim.config.environment import load_environment
5
 from tracim.config.environment import load_environment
6
-from tracim.lib.daemons import DaemonsManager
7
-from tracim.lib.daemons import RadicaleDaemon
6
+from sqlalchemy.pool import NullPool
8
 
7
 
9
 __all__ = ['make_app']
8
 __all__ = ['make_app']
10
 
9
 
33
     
32
     
34
    
33
    
35
     """
34
     """
35
+    # Configure NullPool for SQLAlchemy id we are in tests: unknown reason
36
+    # don't close it's connection during test and exceed postgresql limit.
37
+    if global_conf.get('test') == 'true':
38
+        global_conf['sqlalchemy.poolclass'] = NullPool
39
+
36
     app = make_base_app(global_conf, full_stack=True, **app_conf)
40
     app = make_base_app(global_conf, full_stack=True, **app_conf)
37
     
41
     
38
     # Wrap your base TurboGears 2 application with custom middleware here
42
     # Wrap your base TurboGears 2 application with custom middleware here

+ 2 - 0
tracim/tracim/controllers/calendar.py Zobrazit soubor

2
 import re
2
 import re
3
 import tg
3
 import tg
4
 from tg import tmpl_context
4
 from tg import tmpl_context
5
+from tg.predicates import not_anonymous
5
 
6
 
6
 from tracim.lib.base import BaseController
7
 from tracim.lib.base import BaseController
7
 from tracim.lib.calendar import CalendarManager
8
 from tracim.lib.calendar import CalendarManager
16
     """
17
     """
17
 
18
 
18
     @tg.expose('tracim.templates.calendar.iframe_container')
19
     @tg.expose('tracim.templates.calendar.iframe_container')
20
+    @tg.require(not_anonymous())
19
     def index(self):
21
     def index(self):
20
         user = tmpl_context.identity.get('user')
22
         user = tmpl_context.identity.get('user')
21
         dictified_current_user = Context(CTX.CURRENT_USER).toDict(user)
23
         dictified_current_user = Context(CTX.CURRENT_USER).toDict(user)

+ 2 - 2
tracim/tracim/controllers/content.py Zobrazit soubor

288
         workspace = tmpl_context.workspace
288
         workspace = tmpl_context.workspace
289
 
289
 
290
         try:
290
         try:
291
-            item_saved = False
292
             api = ContentApi(tmpl_context.current_user)
291
             api = ContentApi(tmpl_context.current_user)
293
             item = api.get_one(int(item_id), self._item_type, workspace)
292
             item = api.get_one(int(item_id), self._item_type, workspace)
293
+            label_changed = label != item.label
294
 
294
 
295
             # TODO - D.A. - 2015-03-19
295
             # TODO - D.A. - 2015-03-19
296
             # refactor this method in order to make code easier to understand
296
             # refactor this method in order to make code easier to understand
297
 
297
 
298
             with new_revision(item):
298
             with new_revision(item):
299
 
299
 
300
-                if comment and label:
300
+                if (comment and label) or (not comment and label_changed):
301
                     updated_item = api.update_content(
301
                     updated_item = api.update_content(
302
                         item, label if label else item.label,
302
                         item, label if label else item.label,
303
                         comment if comment else ''
303
                         comment if comment else ''

+ 35 - 0
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po Zobrazit soubor

488
 msgid "You're not allowed to access this resource"
488
 msgid "You're not allowed to access this resource"
489
 msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
489
 msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
490
 
490
 
491
+#: tracim/lib/workspace.py:163 tracim/templates/home.mak:132
492
+#: tracim/templates/master_no_toolbar_no_login.mak:106
493
+#: tracim/templates/admin/user_getone.mak:77
494
+#: tracim/templates/admin/workspace_getall.mak:82
495
+msgid "Workspace"
496
+msgstr "Espace de travail"
497
+
498
+#: tracim/lib/workspace.py:166 tracim/templates/admin/workspace_getone.mak:13
499
+#: tracim/templates/admin/workspace_getone.mak:29
500
+msgid "Workspace {}"
501
+msgstr "Espace de travail {}"
502
+
491
 #: tracim/model/auth.py:97
503
 #: tracim/model/auth.py:97
492
 msgid "Nobody"
504
 msgid "Nobody"
493
 msgstr "Personne"
505
 msgstr "Personne"
1148
 msgid "This calendar URL will work with CalDav compatibles clients"
1160
 msgid "This calendar URL will work with CalDav compatibles clients"
1149
 msgstr "Cette url de calendrier fonctionne avec les logiciels compatibles CalDav"
1161
 msgstr "Cette url de calendrier fonctionne avec les logiciels compatibles CalDav"
1150
 
1162
 
1163
+#: tracim/templates/user_workspace_forms.mak:119
1164
+msgid "Timezone"
1165
+msgstr "Fuseau horaire"
1166
+
1167
+#: tracim/templates/user_workspace_forms.mak:120
1168
+msgid "Dates will be displayed with this timezone"
1169
+msgstr "Les dates seront affichées avec ce fuseau horaire"
1170
+
1151
 #: tracim/templates/user_workspace_widgets.mak:37
1171
 #: tracim/templates/user_workspace_widgets.mak:37
1152
 msgid "No folder found."
1172
 msgid "No folder found."
1153
 msgstr "Aucun dossier."
1173
 msgstr "Aucun dossier."
1469
 "data-toggle=\"collapse\" data-target=\"#add-role-from-existing-user-"
1489
 "data-toggle=\"collapse\" data-target=\"#add-role-from-existing-user-"
1470
 "form\">Ajouter un membre</a>."
1490
 "form\">Ajouter un membre</a>."
1471
 
1491
 
1492
+#: tracim/templates/errors/label_invalid_path.mak:6
1493
+msgid ""
1494
+"Chosen label \"{0}\" is invalid because is in conflict with other "
1495
+"resource."
1496
+msgstr ""
1497
+"Le label \"{0}\" est invalide car il est en conflit avec une autre "
1498
+"ressource."
1499
+
1500
+#: tracim/templates/errors/label_invalid_path.mak:9
1501
+msgid "Go Back"
1502
+msgstr "Retour en arrière"
1503
+
1472
 #: tracim/templates/file/edit.mak:9
1504
 #: tracim/templates/file/edit.mak:9
1473
 msgid "Edit file"
1505
 msgid "Edit file"
1474
 msgstr "Modifier le fichier"
1506
 msgstr "Modifier le fichier"
2764
 #~ msgid "Adress to connect to webdav with:"
2796
 #~ msgid "Adress to connect to webdav with:"
2765
 #~ msgstr "Adresses pour se connecter à webdav sous :"
2797
 #~ msgstr "Adresses pour se connecter à webdav sous :"
2766
 
2798
 
2799
+#~ msgid "created on {}"
2800
+#~ msgstr "créé le {}"
2801
+

+ 37 - 9
tracim/tracim/lib/calendar.py Zobrazit soubor

45
         return _('My personal calendar')
45
         return _('My personal calendar')
46
 
46
 
47
     @classmethod
47
     @classmethod
48
-    def get_base_url(cls):
48
+    def get_base_url(cls, low_level: bool=False) -> str:
49
+        """
50
+        :param low_level: If True, use local ip address with radicale port.
51
+        :return: Radical address base url.
52
+        """
49
         from tracim.config.app_cfg import CFG
53
         from tracim.config.app_cfg import CFG
50
         cfg = CFG.get_instance()
54
         cfg = CFG.get_instance()
51
-        return cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE
55
+
56
+        if not low_level:
57
+            return cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE
58
+
59
+        return 'http://127.0.0.1:{0}'.format(cfg.RADICALE_SERVER_PORT)
52
 
60
 
53
     @classmethod
61
     @classmethod
54
     def get_user_base_url(cls):
62
     def get_user_base_url(cls):
63
         return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE, 'workspace/')
71
         return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE, 'workspace/')
64
 
72
 
65
     @classmethod
73
     @classmethod
66
-    def get_user_calendar_url(cls, user_id: int):
74
+    def get_user_calendar_url(
75
+            cls,
76
+            user_id: int,
77
+            low_level: bool=False,
78
+    ):
67
         user_path = CALENDAR_USER_URL_TEMPLATE.format(id=str(user_id))
79
         user_path = CALENDAR_USER_URL_TEMPLATE.format(id=str(user_id))
68
-        return os.path.join(cls.get_base_url(), user_path)
80
+        return os.path.join(
81
+            cls.get_base_url(low_level=low_level),
82
+            user_path,
83
+        )
69
 
84
 
70
     @classmethod
85
     @classmethod
71
-    def get_workspace_calendar_url(cls, workspace_id: int):
86
+    def get_workspace_calendar_url(
87
+            cls,
88
+            workspace_id: int,
89
+            low_level: bool=False,
90
+    ):
72
         workspace_path = CALENDAR_WORKSPACE_URL_TEMPLATE.format(
91
         workspace_path = CALENDAR_WORKSPACE_URL_TEMPLATE.format(
73
             id=str(workspace_id)
92
             id=str(workspace_id)
74
         )
93
         )
75
-        return os.path.join(cls.get_base_url(), workspace_path)
94
+        return os.path.join(
95
+            cls.get_base_url(low_level=low_level),
96
+            workspace_path,
97
+        )
76
 
98
 
77
     def __init__(self, user: User):
99
     def __init__(self, user: User):
78
         self._user = user
100
         self._user = user
325
             calendar_class,
347
             calendar_class,
326
             related_object_id,
348
             related_object_id,
327
     ) -> None:
349
     ) -> None:
328
-        radicale_base_url = self.get_base_url()
350
+        radicale_base_url = self.get_base_url(low_level=True)
329
         client = caldav.DAVClient(
351
         client = caldav.DAVClient(
330
             radicale_base_url,
352
             radicale_base_url,
331
             username=self._user.email,
353
             username=self._user.email,
332
             password=self._user.auth_token,
354
             password=self._user.auth_token,
333
         )
355
         )
334
         if calendar_class == WorkspaceCalendar:
356
         if calendar_class == WorkspaceCalendar:
335
-            calendar_url = self.get_workspace_calendar_url(related_object_id)
357
+            calendar_url = self.get_workspace_calendar_url(
358
+                related_object_id,
359
+                low_level=True,
360
+            )
336
         elif calendar_class == UserCalendar:
361
         elif calendar_class == UserCalendar:
337
-            calendar_url = self.get_user_calendar_url(related_object_id)
362
+            calendar_url = self.get_user_calendar_url(
363
+                related_object_id,
364
+                low_level=True,
365
+            )
338
         else:
366
         else:
339
             raise Exception('Unknown calendar type {0}'.format(calendar_class))
367
             raise Exception('Unknown calendar type {0}'.format(calendar_class))
340
 
368
 

+ 33 - 0
tracim/tracim/lib/content.py Zobrazit soubor

366
 
366
 
367
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False) -> Content:
367
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False) -> Content:
368
         assert content_type in ContentType.allowed_types()
368
         assert content_type in ContentType.allowed_types()
369
+
370
+        if content_type == ContentType.Folder and not label:
371
+            label = self.generate_folder_label(workspace, parent)
372
+
369
         content = Content()
373
         content = Content()
370
         content.owner = self._user
374
         content.owner = self._user
371
         content.parent = parent
375
         content.parent = parent
522
             content_query = content_query.filter(
526
             content_query = content_query.filter(
523
                 Content.parent_id == parent_folder.content_id,
527
                 Content.parent_id == parent_folder.content_id,
524
             )
528
             )
529
+        else:
530
+            content_query = content_query.filter(
531
+                Content.parent_id == None,
532
+            )
525
 
533
 
526
         # Filter with workspace
534
         # Filter with workspace
527
         content_query = content_query.filter(
535
         content_query = content_query.filter(
567
                     .filter(
575
                     .filter(
568
                         Content.parent_id == folder.content_id,
576
                         Content.parent_id == folder.content_id,
569
                     )
577
                     )
578
+            else:
579
+                folder_query = folder_query \
580
+                    .filter(Content.parent_id == None)
570
 
581
 
571
             # Get thirst corresponding folder
582
             # Get thirst corresponding folder
572
             folder = folder_query \
583
             folder = folder_query \
1066
             )
1077
             )
1067
         )
1078
         )
1068
         return query.one()
1079
         return query.one()
1080
+
1081
+    def generate_folder_label(
1082
+            self,
1083
+            workspace: Workspace,
1084
+            parent: Content=None,
1085
+    ) -> str:
1086
+        """
1087
+        Generate a folder label
1088
+        :param workspace: Future folder workspace
1089
+        :param parent: Parent of foture folder (can be None)
1090
+        :return: Generated folder name
1091
+        """
1092
+        query = self._base_query(workspace=workspace)\
1093
+            .filter(Content.label.ilike('{0}%'.format(
1094
+                _('New folder'),
1095
+            )))
1096
+        if parent:
1097
+            query = query.filter(Content.parent == parent)
1098
+
1099
+        return _('New folder {0}').format(
1100
+            query.count() + 1,
1101
+        )

+ 4 - 1
tracim/tracim/lib/email.py Zobrazit soubor

103
             )
103
             )
104
         message = MIMEMultipart('alternative')
104
         message = MIMEMultipart('alternative')
105
         message['Subject'] = subject
105
         message['Subject'] = subject
106
-        message['From'] = self._global_config.EMAIL_NOTIFICATION_FROM
106
+        message['From'] = '{0} <{1}>'.format(
107
+            self._global_config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL,
108
+            self._global_config.EMAIL_NOTIFICATION_FROM_EMAIL,
109
+        )
107
         message['To'] = user.email
110
         message['To'] = user.email
108
 
111
 
109
         text_template_file_path = self._global_config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT  # nopep8
112
         text_template_file_path = self._global_config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT  # nopep8

+ 19 - 1
tracim/tracim/lib/notifications.py Zobrazit soubor

182
         self._smtp_config = smtp_config
182
         self._smtp_config = smtp_config
183
         self._global_config = global_config
183
         self._global_config = global_config
184
 
184
 
185
+    def _get_sender(self, user: User=None) -> str:
186
+        """
187
+        Return sender string like "Bob Dylan
188
+            (via Tracim) <notification@mail.com>"
189
+        :param user: user to extract display name
190
+        :return: sender string
191
+        """
192
+        if user is None:
193
+            return '{0} <{1}>'.format(
194
+                self._global_config.EMAIL_NOTIFICATION_FROM_DEFAULT_LABEL,
195
+                self._global_config.EMAIL_NOTIFICATION_FROM_EMAIL,
196
+            )
197
+
198
+        return '{0} ({1}) <{2}>'.format(
199
+            user.display_name,
200
+            _('via Tracim'),
201
+            self._global_config.EMAIL_NOTIFICATION_FROM_EMAIL,
202
+        )
185
 
203
 
186
     def notify_content_update(self, event_actor_id: int, event_content_id: int):
204
     def notify_content_update(self, event_actor_id: int, event_content_id: int):
187
         """
205
         """
233
 
251
 
234
             message = MIMEMultipart('alternative')
252
             message = MIMEMultipart('alternative')
235
             message['Subject'] = subject
253
             message['Subject'] = subject
236
-            message['From'] = self._global_config.EMAIL_NOTIFICATION_FROM
254
+            message['From'] = self._get_sender(user)
237
             message['To'] = to_addr
255
             message['To'] = to_addr
238
 
256
 
239
             body_text = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)
257
             body_text = self._build_email_body(self._global_config.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT, role, content, user)

+ 105 - 8
tracim/tracim/lib/webdav/design.py Zobrazit soubor

5
 from tracim.model.data import ContentType
5
 from tracim.model.data import ContentType
6
 from tracim.model import data
6
 from tracim.model import data
7
 
7
 
8
+# FIXME: fix temporaire ...
9
+style = """
10
+.title {
11
+	background:#F5F5F5;
12
+	padding-right:15px;
13
+	padding-left:15px;
14
+	padding-top:10px;
15
+	border-bottom:1px solid #CCCCCC;
16
+	overflow:auto;
17
+} .title h1 { margin-top:0; }
18
+
19
+.content {
20
+	padding: 15px;
21
+}
22
+
23
+#left{ padding:0; }
24
+
25
+#right {
26
+	background:#F5F5F5;
27
+	border-left:1px solid #CCCCCC;
28
+	border-bottom: 1px solid #CCCCCC;
29
+	padding-top:15px;
30
+}
31
+@media (max-width: 1200px) {
32
+	#right {
33
+		border-top:1px solid #CCCCCC;
34
+		border-left: none;
35
+		border-bottom: none;
36
+	}
37
+}
38
+
39
+body { overflow:auto; }
40
+
41
+.btn {
42
+	text-align: left;
43
+}
44
+
45
+.table tbody tr .my-align {
46
+	vertical-align:middle;
47
+}
48
+
49
+.title-icon {
50
+	font-size:2.5em;
51
+	float:left;
52
+	margin-right:10px;
53
+}
54
+.title.page, .title-icon.page { color:#00CC00; }
55
+.title.thread, .title-icon.thread { color:#428BCA; }
56
+
57
+/* ****************************** */
58
+.description-icon {
59
+	color:#999;
60
+	font-size:3em;
61
+}
62
+
63
+.description {
64
+	border-left: 5px solid #999;
65
+	padding-left: 10px;
66
+	margin-left: 10px;
67
+	margin-bottom:10px;
68
+}
69
+
70
+.description-text {
71
+	display:block;
72
+	overflow:hidden;
73
+	color:#999;
74
+}
75
+
76
+.comment-row:nth-child(2n) {
77
+	background-color:#F5F5F5;
78
+}
79
+
80
+.comment-row:nth-child(2n+1) {
81
+	background-color:#FFF;
82
+}
83
+
84
+.comment-icon {
85
+	color:#CCC;
86
+	font-size:3em;
87
+	display:inline-block;
88
+	margin-right: 10px;
89
+	float:left;
90
+}
91
+
92
+.comment-content {
93
+	display:block;
94
+	overflow:hidden;
95
+}
96
+
97
+.comment, .comment-revision {
98
+	padding:10px;
99
+	border-top: 1px solid #999;
100
+}
101
+
102
+.comment-revision-icon {
103
+	color:#777;
104
+	margin-right: 10px;
105
+}
106
+
107
+.title-text {
108
+	display: inline-block;
109
+}
110
+"""
111
+
112
+
8
 def create_readable_date(created, delta_from_datetime: datetime = None):
113
 def create_readable_date(created, delta_from_datetime: datetime = None):
9
     if not delta_from_datetime:
114
     if not delta_from_datetime:
10
         delta_from_datetime = datetime.now()
115
         delta_from_datetime = datetime.now()
29
     return aff
134
     return aff
30
 
135
 
31
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
136
 def designPage(content: data.Content, content_revision: data.ContentRevisionRO) -> str:
32
-    f = open('tracim/lib/webdav/style.css', 'r')
33
-    style = f.read()
34
-    f.close()
35
-
36
     hist = content.get_history()
137
     hist = content.get_history()
37
     histHTML = '<table class="table table-striped table-hover">'
138
     histHTML = '<table class="table table-striped table-hover">'
38
     for event in hist:
139
     for event in hist:
134
     return file
235
     return file
135
 
236
 
136
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
237
 def designThread(content: data.Content, content_revision: data.ContentRevisionRO, comments) -> str:
137
-        f = open('tracim/lib/webdav/style.css', 'r')
138
-        style = f.read()
139
-        f.close()
140
-
141
         hist = content.get_history()
238
         hist = content.get_history()
142
 
239
 
143
         allT = []
240
         allT = []

+ 2 - 1
tracim/tracim/lib/webdav/sql_dav_provider.py Zobrazit soubor

1
 # coding: utf8
1
 # coding: utf8
2
 
2
 
3
 import re
3
 import re
4
-from os.path import basename, dirname, normpath
4
+from os.path import basename, dirname
5
 from sqlalchemy.orm.exc import NoResultFound
5
 from sqlalchemy.orm.exc import NoResultFound
6
 from tracim.lib.webdav.utils import transform_to_bdd
6
 from tracim.lib.webdav.utils import transform_to_bdd
7
 
7
 
18
 from tracim.lib.workspace import WorkspaceApi
18
 from tracim.lib.workspace import WorkspaceApi
19
 from tracim.model.data import Content, Workspace
19
 from tracim.model.data import Content, Workspace
20
 from tracim.model.data import ContentType
20
 from tracim.model.data import ContentType
21
+from tracim.lib.webdav.utils import normpath
21
 
22
 
22
 
23
 
23
 class Provider(DAVProvider):
24
 class Provider(DAVProvider):

+ 2 - 2
tracim/tracim/lib/webdav/sql_resources.py Zobrazit soubor

5
 import re
5
 import re
6
 from datetime import datetime
6
 from datetime import datetime
7
 from time import mktime
7
 from time import mktime
8
-from os.path import normpath, dirname, basename
9
-import mimetypes
8
+from os.path import dirname, basename
10
 
9
 
11
 from tracim.lib.content import ContentApi
10
 from tracim.lib.content import ContentApi
12
 from tracim.lib.user import UserApi
11
 from tracim.lib.user import UserApi
24
 from wsgidav.dav_error import DAVError, HTTP_FORBIDDEN
23
 from wsgidav.dav_error import DAVError, HTTP_FORBIDDEN
25
 from wsgidav.dav_provider import DAVCollection, DAVNonCollection
24
 from wsgidav.dav_provider import DAVCollection, DAVNonCollection
26
 from wsgidav.dav_provider import _DAVResource
25
 from wsgidav.dav_provider import _DAVResource
26
+from tracim.lib.webdav.utils import normpath
27
 
27
 
28
 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
28
 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
29
 
29
 

+ 9 - 0
tracim/tracim/lib/webdav/utils.py Zobrazit soubor

8
 from datetime import datetime
8
 from datetime import datetime
9
 
9
 
10
 import yaml
10
 import yaml
11
+from os.path import normpath as base_normpath
11
 from wsgidav import util
12
 from wsgidav import util
12
 from wsgidav import compat
13
 from wsgidav import compat
13
 from wsgidav.middleware import BaseMiddleware
14
 from wsgidav.middleware import BaseMiddleware
274
                 dump_content['content'] = ElementTree.tostring(xml, 'utf-8')
275
                 dump_content['content'] = ElementTree.tostring(xml, 'utf-8')
275
 
276
 
276
             f.write(yaml.dump(dump_content, default_flow_style=False))
277
             f.write(yaml.dump(dump_content, default_flow_style=False))
278
+
279
+
280
+def normpath(path):
281
+    if path == b'':
282
+        path = b'/'
283
+    elif path == '':
284
+        path = '/'
285
+    return base_normpath(path)

+ 23 - 2
tracim/tracim/lib/workspace.py Zobrazit soubor

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import transaction
2
 import transaction
3
+
3
 from sqlalchemy.orm import Query
4
 from sqlalchemy.orm import Query
5
+from tg.i18n import ugettext as _
4
 
6
 
5
 from tracim.lib.userworkspace import RoleApi
7
 from tracim.lib.userworkspace import RoleApi
6
 from tracim.model.auth import Group
8
 from tracim.model.auth import Group
17
     def __init__(self, current_user: User):
19
     def __init__(self, current_user: User):
18
         self._user = current_user
20
         self._user = current_user
19
 
21
 
22
+    def _base_query_without_roles(self):
23
+        return DBSession.query(Workspace).filter(Workspace.is_deleted==False)
24
+
20
     def _base_query(self):
25
     def _base_query(self):
21
         if self._user.profile.id>=Group.TIM_ADMIN:
26
         if self._user.profile.id>=Group.TIM_ADMIN:
22
-            return DBSession.query(Workspace).filter(Workspace.is_deleted==False)
27
+            return self._base_query_without_roles()
23
 
28
 
24
         return DBSession.query(Workspace).\
29
         return DBSession.query(Workspace).\
25
             join(Workspace.roles).\
30
             join(Workspace.roles).\
28
 
33
 
29
     def create_workspace(
34
     def create_workspace(
30
             self,
35
             self,
31
-            label: str,
36
+            label: str='',
32
             description: str='',
37
             description: str='',
33
             calendar_enabled: bool=False,
38
             calendar_enabled: bool=False,
34
             save_now: bool=False,
39
             save_now: bool=False,
35
     ) -> Workspace:
40
     ) -> Workspace:
41
+        if not label:
42
+            label = self.generate_label()
43
+
36
         workspace = Workspace()
44
         workspace = Workspace()
37
         workspace.label = label
45
         workspace.label = label
38
         workspace.description = description
46
         workspace.description = description
146
     def get_base_query(self) -> Query:
154
     def get_base_query(self) -> Query:
147
         return self._base_query()
155
         return self._base_query()
148
 
156
 
157
+    def generate_label(self) -> str:
158
+        """
159
+        :return: Generated workspace label
160
+        """
161
+        query = self._base_query_without_roles() \
162
+            .filter(Workspace.label.ilike('{0}%'.format(
163
+                _('Workspace'),
164
+            )))
165
+
166
+        return _('Workspace {}').format(
167
+            query.count() + 1,
168
+        )
169
+
149
 
170
 
150
 class UnsafeWorkspaceApi(WorkspaceApi):
171
 class UnsafeWorkspaceApi(WorkspaceApi):
151
     def _base_query(self):
172
     def _base_query(self):

+ 2 - 1
tracim/tracim/model/__init__.py Zobrazit soubor

85
 
85
 
86
 def init_model(engine):
86
 def init_model(engine):
87
     """Call me before using any of the tables or classes in the model."""
87
     """Call me before using any of the tables or classes in the model."""
88
-    DBSession.configure(bind=engine)
88
+    if not DBSession.registry.has():  # Prevent a SQLAlchemy warning
89
+        DBSession.configure(bind=engine)
89
 
90
 
90
     # If you are using reflection to introspect your database and create
91
     # If you are using reflection to introspect your database and create
91
     # table objects for you, your tables must be defined and mapped inside
92
     # table objects for you, your tables must be defined and mapped inside

+ 51 - 9
tracim/tracim/public/_caldavzap/forms.js Zobrazit soubor

1666
 	var cals=globalResourceCalDAVList.sortedCollections;
1666
 	var cals=globalResourceCalDAVList.sortedCollections;
1667
 	var calendarObj = $('#event_calendar');
1667
 	var calendarObj = $('#event_calendar');
1668
 	var calSelected = $('.resourceCalDAV_item.resourceCalDAV_item_selected').attr('data-id');
1668
 	var calSelected = $('.resourceCalDAV_item.resourceCalDAV_item_selected').attr('data-id');
1669
-		for(var i=0;i<cals.length;i++)
1670
-		{
1671
-			if(cals[i].uid!=undefined && ((calEvent!=null && calEvent.res_id==cals[i].uid) || (cals[i].makeLoaded && !cals[i].permissions_read_only )))
1672
-			{
1673
-				calendarObj.append(new Option(cals[i].displayValue,cals[i].uid));
1674
-			}
1675
-		}
1669
+
1670
+  var calendarsApiHasResponded = false
1671
+	// begin custom code
1672
+  $.ajax({
1673
+    url: '/api/calendars/',
1674
+    method: 'GET',
1675
+    contentType: 'application/json'
1676
+  }).done(function (data) {
1677
+
1678
+    var regExpUser = new RegExp('\/user\/')
1679
+    var regExpWorkspace = new RegExp('\/workspace\/')
1680
+
1681
+    var user_or_workspace
1682
+
1683
+    for(var i=0;i<cals.length;i++)
1684
+    {
1685
+      if(cals[i].uid!=undefined && ((calEvent!=null && calEvent.res_id==cals[i].uid) || (cals[i].makeLoaded && !cals[i].permissions_read_only )))
1686
+      {
1687
+        var currentICS = parseInt(cals[i].displayValue.replace('.ics', ''))
1688
+
1689
+        if (regExpUser.test(cals[i].uid))
1690
+          user_or_workspace = 'user'
1691
+        else if (regExpWorkspace.test(cals[i].uid))
1692
+          user_or_workspace = 'workspace'
1693
+        else
1694
+          user_or_workspace = 'fail'
1695
+
1696
+        var calName = ''
1697
+        var calList_length = data.value_list.length
1698
+        for (var j = 0; j < calList_length; j++) {
1699
+          if (data.value_list[j].id === currentICS && data.value_list[j].type === user_or_workspace) {
1700
+            calName = data.value_list[j].label
1701
+          }
1702
+        }
1703
+
1704
+        calendarObj.append(new Option(calName, cals[i].uid));
1705
+      }
1706
+    }
1707
+    calendarsApiHasResponded = true
1708
+  })
1676
 
1709
 
1677
 	if(mod=='new')
1710
 	if(mod=='new')
1678
 	{
1711
 	{
2362
 			$('#event_details_template').find('svg[data-type="select_icon"]').replaceWith($('<div>').append($(newSVG).clone()).html());
2395
 			$('#event_details_template').find('svg[data-type="select_icon"]').replaceWith($('<div>').append($(newSVG).clone()).html());
2363
 		}
2396
 		}
2364
 		/*************************** END OF BAD HACKS SECTION ***************************/
2397
 		/*************************** END OF BAD HACKS SECTION ***************************/
2365
-		if(calEvent.etag!='')
2366
-			$('#event_calendar').val(calEvent.res_id);
2398
+		if(calEvent.etag!='') {
2399
+      var interval = window.setInterval(function () {
2400
+        if (calendarsApiHasResponded === true) {
2401
+          $('#event_calendar').val(calEvent.res_id);
2402
+          stopInterval()
2403
+        }
2404
+      }, 500)
2405
+      var stopInterval = function () {
2406
+        window.clearInterval(interval)
2407
+      }
2408
+    }
2367
 	}
2409
 	}
2368
 
2410
 
2369
 	if(repeatOne=='editOnly' || $('#recurrenceID').val()!='')
2411
 	if(repeatOne=='editOnly' || $('#recurrenceID').val()!='')

+ 2 - 2
tracim/tracim/public/_caldavzap/resource.js Zobrazit soubor

503
     }).done(function (data) {
503
     }).done(function (data) {
504
       var currentICS = parseInt(inputResource.displayvalue.replace('.ics', ''))
504
       var currentICS = parseInt(inputResource.displayvalue.replace('.ics', ''))
505
 
505
 
506
-      var regExpUser = new RegExp('\/cal\/user\/')
507
-      var regExpWorkspace = new RegExp('\/cal\/workspace\/')
506
+      var regExpUser = new RegExp('\/user\/')
507
+      var regExpWorkspace = new RegExp('\/workspace\/')
508
 
508
 
509
       var user_or_workspace
509
       var user_or_workspace
510
 
510
 

+ 8 - 0
tracim/tracim/public/assets/css/dashboard.css Zobrazit soubor

391
   font-size: 12px;
391
   font-size: 12px;
392
 }
392
 }
393
 
393
 
394
+.user-edit-form__calendar__url {
395
+  padding: 8px 15px;
396
+  border: 1px solid #ccc;
397
+  border-radius: 4px;
398
+  background-color: #eee;
399
+  color: #555;
400
+}
401
+
394
 .folder__filter {
402
 .folder__filter {
395
   font-size: 14px;
403
   font-size: 14px;
396
 }
404
 }

+ 1 - 0
tracim/tracim/public/assets/js/main.js Zobrazit soubor

13
       }
13
       }
14
     })
14
     })
15
   }
15
   }
16
+
16
 })
17
 })

+ 1 - 3
tracim/tracim/templates/thread/getone.mak Zobrazit soubor

141
 % for event in result.thread.history:
141
 % for event in result.thread.history:
142
     ## TODO - D.A. - 2015-08-20
142
     ## TODO - D.A. - 2015-08-20
143
     ## Allow to show full history (with status change and archive/unarchive)
143
     ## Allow to show full history (with status change and archive/unarchive)
144
-    % if event.type.id in ('comment', 'creation'):
145
-        ${WIDGETS.SECURED_HISTORY_VIRTUAL_EVENT(fake_api.current_user, event)}
146
-    % endif
144
+    ${WIDGETS.SECURED_HISTORY_VIRTUAL_EVENT(fake_api.current_user, event)}
147
 % endfor
145
 % endfor
148
 
146
 
149
 ## % for comment in result.thread.comments:
147
 ## % for comment in result.thread.comments:

+ 6 - 0
tracim/tracim/templates/user_edit_me.mak Zobrazit soubor

5
 
5
 
6
 ${FORMS.USER_EDIT_FORM('user-edit-form', result.user, tg.url('/user/{}?_method=PUT'.format(result.user.id)), next_url=fake_api.next_url)}
6
 ${FORMS.USER_EDIT_FORM('user-edit-form', result.user, tg.url('/user/{}?_method=PUT'.format(result.user.id)), next_url=fake_api.next_url)}
7
 
7
 
8
+<script type="text/javascript">
9
+  // add select2 for timezone in user edit profile modale
10
+  $('#timezone').select2({
11
+    dropdownParent: $("#user-edit-form") // this is a workaround to make select works inside bootstrap modal
12
+  })
13
+</script>

+ 3 - 3
tracim/tracim/templates/user_workspace_forms.mak Zobrazit soubor

57
                 <span class="pull-right t-modal-form-submit-button">
57
                 <span class="pull-right t-modal-form-submit-button">
58
                     <button id="${dom_id}-submit-button" type="submit" class="btn btn-small btn-success" title="${_('Create this page')}"><i class=" fa fa-check"></i> ${_('Validate')}</button>
58
                     <button id="${dom_id}-submit-button" type="submit" class="btn btn-small btn-success" title="${_('Create this page')}"><i class=" fa fa-check"></i> ${_('Validate')}</button>
59
                 </span>
59
                 </span>
60
-                
60
+
61
                 <div style="clear: both;"></div>
61
                 <div style="clear: both;"></div>
62
             </form>
62
             </form>
63
         </div>
63
         </div>
80
                 <span class="pull-right t-modal-form-submit-button">
80
                 <span class="pull-right t-modal-form-submit-button">
81
                     <button id="${dom_id}-submit-button" type="submit" class="btn btn-small btn-success" title="${_('Create this page')}"><i class=" fa fa-check"></i> ${_('Validate')}</button>
81
                     <button id="${dom_id}-submit-button" type="submit" class="btn btn-small btn-success" title="${_('Create this page')}"><i class=" fa fa-check"></i> ${_('Validate')}</button>
82
                 </span>
82
                 </span>
83
-                
83
+
84
                 <div style="clear: both;"></div>
84
                 <div style="clear: both;"></div>
85
             </form>
85
             </form>
86
         </div>
86
         </div>
113
             <div class="form-group">
113
             <div class="form-group">
114
                 <label for="calendar">${_('Personal calendar')}</label>
114
                 <label for="calendar">${_('Personal calendar')}</label>
115
                 <span class="info readonly">${_('This calendar URL will work with CalDav compatibles clients')}</span>
115
                 <span class="info readonly">${_('This calendar URL will work with CalDav compatibles clients')}</span>
116
-                <input id="calendar" type="text" class="form-control"  disabled="disabled" value="${user.calendar_url}" />
116
+                <div class="user-edit-form__calendar__url">${user.calendar_url}</div>
117
             </div>
117
             </div>
118
             <div class="form-group">
118
             <div class="form-group">
119
                 <label for="timezone">${_('Timezone')}</label>
119
                 <label for="timezone">${_('Timezone')}</label>

+ 13 - 1
tracim/tracim/tests/__init__.py Zobrazit soubor

63
 
63
 
64
 def load_app(name=application_name):
64
 def load_app(name=application_name):
65
     """Load the test application."""
65
     """Load the test application."""
66
-    return TestApp(loadapp('config:test.ini#%s' % name, relative_to=getcwd()))
66
+    return TestApp(
67
+        loadapp(
68
+            'config:test.ini#%s' % name,
69
+            relative_to=getcwd(),
70
+            global_conf={
71
+                'test': 'true',
72
+            },
73
+        )
74
+    )
67
 
75
 
68
 
76
 
69
 def setup_app(section_name=None):
77
 def setup_app(section_name=None):
156
             logger.debug(teardown_db, 'Exception while trying to remove sequence {}'.format(sequence.name))
164
             logger.debug(teardown_db, 'Exception while trying to remove sequence {}'.format(sequence.name))
157
 
165
 
158
     transaction.commit()
166
     transaction.commit()
167
+    connection.close()
159
     engine.dispose()
168
     engine.dispose()
160
 
169
 
161
 
170
 
249
         DBSession.close()
258
         DBSession.close()
250
         daemons.execute_in_thread('radicale', lambda: transaction.commit())
259
         daemons.execute_in_thread('radicale', lambda: transaction.commit())
251
         teardown_db()
260
         teardown_db()
261
+        transaction.commit()
262
+        DBSession.close_all()
263
+        config['tg.app_globals'].sa_engine.dispose()
252
 
264
 
253
 
265
 
254
 class TracimTestController(TestController):
266
 class TracimTestController(TestController):