Преглед изворни кода

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

Come пре 7 година
родитељ
комит
7302218264

+ 2 - 1
README.md Прегледај датотеку

@@ -318,7 +318,8 @@ The reset password related parameters are the follwoing ones :
318 318
 The main parameters for notifications are the following ones:
319 319
 
320 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 323
     email.notification.smtp.server = smtp.mycompany.com
323 324
     email.notification.smtp.port = 25
324 325
     email.notification.smtp.user = username

+ 1 - 1
install/requirements.txt Прегледај датотеку

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

+ 2 - 1
tracim/development.ini.base Прегледај датотеку

@@ -176,7 +176,8 @@ website.base_url = http://127.0.0.1:8080
176 176
 website.server_name = 127.0.0.1
177 177
     
178 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 181
 email.notification.content_update.template.html = ./tracim/templates/mail/content_update_body_html.mak
181 182
 email.notification.content_update.template.text = ./tracim/templates/mail/content_update_body_text.mak
182 183
 email.notification.created_account.template.html = ./tracim/templates/mail/created_account_body_html.mak

+ 1 - 0
tracim/migration/env.py Прегледај датотеку

@@ -65,6 +65,7 @@ def run_migrations_online():
65 65
             context.run_migrations()
66 66
     finally:
67 67
         connection.close()
68
+        engine.dispose()
68 69
 
69 70
 if context.is_offline_mode():
70 71
     run_migrations_offline()

+ 13 - 1
tracim/tracim/config/app_cfg.py Прегледај датотеку

@@ -202,8 +202,17 @@ class CFG(object):
202 202
         self.WEBSITE_SUBTITLE = tg.config.get('website.home.subtitle', '')
203 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 216
         self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_HTML = tg.config.get('email.notification.content_update.template.html')
208 217
         self.EMAIL_NOTIFICATION_CONTENT_UPDATE_TEMPLATE_TEXT = tg.config.get('email.notification.content_update.template.text')
209 218
         self.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_HTML = tg.config.get(
@@ -327,6 +336,9 @@ class CFG(object):
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 343
     def get_tracker_js_content(self, js_tracker_file_path = None):
332 344
         js_tracker_file_path = tg.config.get('js_tracker_path', None)

+ 6 - 2
tracim/tracim/config/middleware.py Прегледај датотеку

@@ -3,8 +3,7 @@
3 3
 
4 4
 from tracim.config.app_cfg import base_config
5 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 8
 __all__ = ['make_app']
10 9
 
@@ -33,6 +32,11 @@ def make_app(global_conf, full_stack=True, **app_conf):
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 40
     app = make_base_app(global_conf, full_stack=True, **app_conf)
37 41
     
38 42
     # Wrap your base TurboGears 2 application with custom middleware here

+ 2 - 0
tracim/tracim/controllers/calendar.py Прегледај датотеку

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

+ 2 - 2
tracim/tracim/controllers/content.py Прегледај датотеку

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

+ 35 - 0
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po Прегледај датотеку

@@ -488,6 +488,18 @@ msgstr "Vous n'êtes pas autorisé à accéder à cette ressource"
488 488
 msgid "You're not allowed to access this resource"
489 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 503
 #: tracim/model/auth.py:97
492 504
 msgid "Nobody"
493 505
 msgstr "Personne"
@@ -1148,6 +1160,14 @@ msgstr "Calendrier personnel"
1148 1160
 msgid "This calendar URL will work with CalDav compatibles clients"
1149 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 1171
 #: tracim/templates/user_workspace_widgets.mak:37
1152 1172
 msgid "No folder found."
1153 1173
 msgstr "Aucun dossier."
@@ -1469,6 +1489,18 @@ msgstr ""
1469 1489
 "data-toggle=\"collapse\" data-target=\"#add-role-from-existing-user-"
1470 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 1504
 #: tracim/templates/file/edit.mak:9
1473 1505
 msgid "Edit file"
1474 1506
 msgstr "Modifier le fichier"
@@ -2764,3 +2796,6 @@ msgstr "Supprimer l'espace de travail"
2764 2796
 #~ msgid "Adress to connect to webdav with:"
2765 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 Прегледај датотеку

@@ -45,10 +45,18 @@ class CalendarManager(object):
45 45
         return _('My personal calendar')
46 46
 
47 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 53
         from tracim.config.app_cfg import CFG
50 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 61
     @classmethod
54 62
     def get_user_base_url(cls):
@@ -63,16 +71,30 @@ class CalendarManager(object):
63 71
         return os.path.join(cfg.RADICALE_CLIENT_BASE_URL_TEMPLATE, 'workspace/')
64 72
 
65 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 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 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 91
         workspace_path = CALENDAR_WORKSPACE_URL_TEMPLATE.format(
73 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 99
     def __init__(self, user: User):
78 100
         self._user = user
@@ -325,16 +347,22 @@ class CalendarManager(object):
325 347
             calendar_class,
326 348
             related_object_id,
327 349
     ) -> None:
328
-        radicale_base_url = self.get_base_url()
350
+        radicale_base_url = self.get_base_url(low_level=True)
329 351
         client = caldav.DAVClient(
330 352
             radicale_base_url,
331 353
             username=self._user.email,
332 354
             password=self._user.auth_token,
333 355
         )
334 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 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 366
         else:
339 367
             raise Exception('Unknown calendar type {0}'.format(calendar_class))
340 368
 

+ 33 - 0
tracim/tracim/lib/content.py Прегледај датотеку

@@ -366,6 +366,10 @@ class ContentApi(object):
366 366
 
367 367
     def create(self, content_type: str, workspace: Workspace, parent: Content=None, label:str ='', do_save=False, is_temporary: bool=False) -> Content:
368 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 373
         content = Content()
370 374
         content.owner = self._user
371 375
         content.parent = parent
@@ -522,6 +526,10 @@ class ContentApi(object):
522 526
             content_query = content_query.filter(
523 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 534
         # Filter with workspace
527 535
         content_query = content_query.filter(
@@ -567,6 +575,9 @@ class ContentApi(object):
567 575
                     .filter(
568 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 582
             # Get thirst corresponding folder
572 583
             folder = folder_query \
@@ -1066,3 +1077,25 @@ class ContentApi(object):
1066 1077
             )
1067 1078
         )
1068 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 Прегледај датотеку

@@ -103,7 +103,10 @@ class EmailManager(object):
103 103
             )
104 104
         message = MIMEMultipart('alternative')
105 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 110
         message['To'] = user.email
108 111
 
109 112
         text_template_file_path = self._global_config.EMAIL_NOTIFICATION_CREATED_ACCOUNT_TEMPLATE_TEXT  # nopep8

+ 19 - 1
tracim/tracim/lib/notifications.py Прегледај датотеку

@@ -182,6 +182,24 @@ class EmailNotifier(object):
182 182
         self._smtp_config = smtp_config
183 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 204
     def notify_content_update(self, event_actor_id: int, event_content_id: int):
187 205
         """
@@ -233,7 +251,7 @@ class EmailNotifier(object):
233 251
 
234 252
             message = MIMEMultipart('alternative')
235 253
             message['Subject'] = subject
236
-            message['From'] = self._global_config.EMAIL_NOTIFICATION_FROM
254
+            message['From'] = self._get_sender(user)
237 255
             message['To'] = to_addr
238 256
 
239 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 Прегледај датотеку

@@ -5,6 +5,111 @@ from tracim.model.data import VirtualEvent
5 5
 from tracim.model.data import ContentType
6 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 113
 def create_readable_date(created, delta_from_datetime: datetime = None):
9 114
     if not delta_from_datetime:
10 115
         delta_from_datetime = datetime.now()
@@ -29,10 +134,6 @@ def create_readable_date(created, delta_from_datetime: datetime = None):
29 134
     return aff
30 135
 
31 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 137
     hist = content.get_history()
37 138
     histHTML = '<table class="table table-striped table-hover">'
38 139
     for event in hist:
@@ -134,10 +235,6 @@ def designPage(content: data.Content, content_revision: data.ContentRevisionRO)
134 235
     return file
135 236
 
136 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 238
         hist = content.get_history()
142 239
 
143 240
         allT = []

+ 2 - 1
tracim/tracim/lib/webdav/sql_dav_provider.py Прегледај датотеку

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

+ 2 - 2
tracim/tracim/lib/webdav/sql_resources.py Прегледај датотеку

@@ -5,8 +5,7 @@ import transaction
5 5
 import re
6 6
 from datetime import datetime
7 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 10
 from tracim.lib.content import ContentApi
12 11
 from tracim.lib.user import UserApi
@@ -24,6 +23,7 @@ from wsgidav import compat
24 23
 from wsgidav.dav_error import DAVError, HTTP_FORBIDDEN
25 24
 from wsgidav.dav_provider import DAVCollection, DAVNonCollection
26 25
 from wsgidav.dav_provider import _DAVResource
26
+from tracim.lib.webdav.utils import normpath
27 27
 
28 28
 from sqlalchemy.orm.exc import NoResultFound, MultipleResultsFound
29 29
 

+ 9 - 0
tracim/tracim/lib/webdav/utils.py Прегледај датотеку

@@ -8,6 +8,7 @@ import time
8 8
 from datetime import datetime
9 9
 
10 10
 import yaml
11
+from os.path import normpath as base_normpath
11 12
 from wsgidav import util
12 13
 from wsgidav import compat
13 14
 from wsgidav.middleware import BaseMiddleware
@@ -274,3 +275,11 @@ class TracimWsgiDavDebugFilter(BaseMiddleware):
274 275
                 dump_content['content'] = ElementTree.tostring(xml, 'utf-8')
275 276
 
276 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 Прегледај датотеку

@@ -1,6 +1,8 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import transaction
3
+
3 4
 from sqlalchemy.orm import Query
5
+from tg.i18n import ugettext as _
4 6
 
5 7
 from tracim.lib.userworkspace import RoleApi
6 8
 from tracim.model.auth import Group
@@ -17,9 +19,12 @@ class WorkspaceApi(object):
17 19
     def __init__(self, current_user: User):
18 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 25
     def _base_query(self):
21 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 29
         return DBSession.query(Workspace).\
25 30
             join(Workspace.roles).\
@@ -28,11 +33,14 @@ class WorkspaceApi(object):
28 33
 
29 34
     def create_workspace(
30 35
             self,
31
-            label: str,
36
+            label: str='',
32 37
             description: str='',
33 38
             calendar_enabled: bool=False,
34 39
             save_now: bool=False,
35 40
     ) -> Workspace:
41
+        if not label:
42
+            label = self.generate_label()
43
+
36 44
         workspace = Workspace()
37 45
         workspace.label = label
38 46
         workspace.description = description
@@ -146,6 +154,19 @@ class WorkspaceApi(object):
146 154
     def get_base_query(self) -> Query:
147 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 171
 class UnsafeWorkspaceApi(WorkspaceApi):
151 172
     def _base_query(self):

+ 2 - 1
tracim/tracim/model/__init__.py Прегледај датотеку

@@ -85,7 +85,8 @@ metadata = DeclarativeBase.metadata
85 85
 
86 86
 def init_model(engine):
87 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 91
     # If you are using reflection to introspect your database and create
91 92
     # table objects for you, your tables must be defined and mapped inside

+ 51 - 9
tracim/tracim/public/_caldavzap/forms.js Прегледај датотеку

@@ -1666,13 +1666,46 @@ function showEventForm(date, allDay, calEvent, jsEvent, mod, repeatOne, confirmR
1666 1666
 	var cals=globalResourceCalDAVList.sortedCollections;
1667 1667
 	var calendarObj = $('#event_calendar');
1668 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 1710
 	if(mod=='new')
1678 1711
 	{
@@ -2362,8 +2395,17 @@ function showEventForm(date, allDay, calEvent, jsEvent, mod, repeatOne, confirmR
2362 2395
 			$('#event_details_template').find('svg[data-type="select_icon"]').replaceWith($('<div>').append($(newSVG).clone()).html());
2363 2396
 		}
2364 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 2411
 	if(repeatOne=='editOnly' || $('#recurrenceID').val()!='')

+ 2 - 2
tracim/tracim/public/_caldavzap/resource.js Прегледај датотеку

@@ -503,8 +503,8 @@ function ResourceCalDAVList()
503 503
     }).done(function (data) {
504 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 509
       var user_or_workspace
510 510
 

+ 8 - 0
tracim/tracim/public/assets/css/dashboard.css Прегледај датотеку

@@ -391,6 +391,14 @@ span.info.readonly {
391 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 402
 .folder__filter {
395 403
   font-size: 14px;
396 404
 }

+ 1 - 0
tracim/tracim/public/assets/js/main.js Прегледај датотеку

@@ -13,4 +13,5 @@ $(document).ready(function () {
13 13
       }
14 14
     })
15 15
   }
16
+
16 17
 })

+ 1 - 3
tracim/tracim/templates/thread/getone.mak Прегледај датотеку

@@ -141,9 +141,7 @@
141 141
 % for event in result.thread.history:
142 142
     ## TODO - D.A. - 2015-08-20
143 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 145
 % endfor
148 146
 
149 147
 ## % for comment in result.thread.comments:

+ 6 - 0
tracim/tracim/templates/user_edit_me.mak Прегледај датотеку

@@ -5,3 +5,9 @@
5 5
 
6 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 Прегледај датотеку

@@ -57,7 +57,7 @@
57 57
                 <span class="pull-right t-modal-form-submit-button">
58 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 59
                 </span>
60
-                
60
+
61 61
                 <div style="clear: both;"></div>
62 62
             </form>
63 63
         </div>
@@ -80,7 +80,7 @@
80 80
                 <span class="pull-right t-modal-form-submit-button">
81 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 82
                 </span>
83
-                
83
+
84 84
                 <div style="clear: both;"></div>
85 85
             </form>
86 86
         </div>
@@ -113,7 +113,7 @@
113 113
             <div class="form-group">
114 114
                 <label for="calendar">${_('Personal calendar')}</label>
115 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 117
             </div>
118 118
             <div class="form-group">
119 119
                 <label for="timezone">${_('Timezone')}</label>

+ 13 - 1
tracim/tracim/tests/__init__.py Прегледај датотеку

@@ -63,7 +63,15 @@ class TestApp(BaseTestApp):
63 63
 
64 64
 def load_app(name=application_name):
65 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 77
 def setup_app(section_name=None):
@@ -156,6 +164,7 @@ def teardown_db():
156 164
             logger.debug(teardown_db, 'Exception while trying to remove sequence {}'.format(sequence.name))
157 165
 
158 166
     transaction.commit()
167
+    connection.close()
159 168
     engine.dispose()
160 169
 
161 170
 
@@ -249,6 +258,9 @@ class TestController(object):
249 258
         DBSession.close()
250 259
         daemons.execute_in_thread('radicale', lambda: transaction.commit())
251 260
         teardown_db()
261
+        transaction.commit()
262
+        DBSession.close_all()
263
+        config['tg.app_globals'].sa_engine.dispose()
252 264
 
253 265
 
254 266
 class TracimTestController(TestController):