Browse Source

Merge branch '118/dev/v1.0_beta/user_timezone' of https://github.com/buxx/tracim into buxx-118/dev/v1.0_beta/user_timezone

Bastien Sevajol (Algoo) 7 years ago
parent
commit
aff69a9353

+ 26 - 0
tracim/migration/versions/2cd20ff3d23a_user_timezone.py View File

@@ -0,0 +1,26 @@
1
+"""user_timezone
2
+
3
+Revision ID: 2cd20ff3d23a
4
+Revises: b4b8d57b54e5
5
+Create Date: 2016-11-08 11:32:00.903232
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '2cd20ff3d23a'
11
+down_revision = 'b4b8d57b54e5'
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    ### commands auto generated by Alembic - please adjust! ###
19
+    op.add_column('users', sa.Column('timezone', sa.Unicode(length=255), server_default='', nullable=False))
20
+    ### end Alembic commands ###
21
+
22
+
23
+def downgrade():
24
+    ### commands auto generated by Alembic - please adjust! ###
25
+    op.drop_column('users', 'timezone')
26
+    ### end Alembic commands ###

+ 9 - 3
tracim/tracim/controllers/user.py View File

@@ -1,4 +1,5 @@
1 1
 # -*- coding: utf-8 -*-
2
+import pytz
2 3
 from webob.exc import HTTPForbidden
3 4
 import tg
4 5
 from tg import tmpl_context
@@ -157,10 +158,14 @@ class UserRestController(TIMRestController):
157 158
 
158 159
         dictified_user = Context(CTX.USER).toDict(current_user, 'user')
159 160
         fake_api = DictLikeClass(next_url=next_url)
160
-        return DictLikeClass(result=dictified_user, fake_api=fake_api)
161
+        return DictLikeClass(
162
+            result=dictified_user,
163
+            fake_api=fake_api,
164
+            timezones=pytz.all_timezones,
165
+        )
161 166
 
162 167
     @tg.expose('tracim.templates.workspace.edit')
163
-    def put(self, user_id, name, email, next_url=None):
168
+    def put(self, user_id, name, email, timezone, next_url=None):
164 169
         user_id = tmpl_context.current_user.user_id
165 170
         current_user = tmpl_context.current_user
166 171
         assert user_id==current_user.user_id
@@ -168,7 +173,8 @@ class UserRestController(TIMRestController):
168 173
         # Only keep allowed field update
169 174
         updated_fields = self._clean_update_fields({
170 175
             'name': name,
171
-            'email': email
176
+            'email': email,
177
+            'timezone': timezone,
172 178
         })
173 179
 
174 180
         api = UserApi(tmpl_context.current_user)

BIN
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.mo View File


File diff suppressed because it is too large
+ 509 - 356
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po


+ 0 - 1
tracim/tracim/lib/app_globals.py View File

@@ -23,7 +23,6 @@ class Globals(object):
23 23
         pass
24 24
 
25 25
     VERSION_NUMBER = '1.0.3'
26
-    LONG_DATE_FORMAT = '%A, the %d of %B %Y at %H:%M'
27 26
     SHORT_DATE_FORMAT = l_('%B %d at %I:%M%p')
28 27
 
29 28
 

+ 37 - 13
tracim/tracim/lib/helpers.py View File

@@ -6,11 +6,14 @@
6 6
 
7 7
 import datetime
8 8
 
9
+import pytz
9 10
 import slugify
10
-from babel.dates import format_date, format_time
11
+from babel.dates import format_date
12
+from babel.dates import format_time
11 13
 from markupsafe import Markup
12 14
 
13 15
 import tg
16
+from tg import tmpl_context
14 17
 from tg.i18n import ugettext as _
15 18
 
16 19
 from tracim.lib import app_globals as plag
@@ -20,7 +23,6 @@ from tracim.lib.base import logger
20 23
 from tracim.lib.content import ContentApi
21 24
 from tracim.lib.userworkspace import RoleApi
22 25
 from tracim.lib.workspace import WorkspaceApi
23
-from tracim.model import User
24 26
 
25 27
 from tracim.model.data import ContentStatus
26 28
 from tracim.model.data import Content
@@ -28,6 +30,39 @@ from tracim.model.data import ContentType
28 30
 from tracim.model.data import UserRoleInWorkspace
29 31
 from tracim.model.data import Workspace
30 32
 
33
+
34
+def get_with_timezone(
35
+        datetime_object: datetime.datetime,
36
+        to_timezone: str='',
37
+        default_from_timezone: str='UTC',
38
+) -> datetime.datetime:
39
+    """
40
+    Change timezone of a date
41
+    :param datetime_object: datetime to update
42
+    :param to_timezone: timezone name, if equal to '',
43
+    try to grap current user timezone. If no given timezone name and no
44
+    current user timezone, return original date time
45
+    :param default_from_timezone: datetime original timezone if datetime
46
+    object is naive
47
+    :return: datetime updated
48
+    """
49
+    # If no to_timezone, try to grab from current user
50
+    if not to_timezone and tmpl_context.current_user:
51
+        to_timezone = tmpl_context.current_user.timezone
52
+
53
+    # If no to_timezone, return original datetime
54
+    if not to_timezone:
55
+        return datetime_object
56
+
57
+    # If datetime_object have not timezone, set new from default_from_timezone
58
+    if not datetime_object.tzinfo:
59
+        from_tzinfo = pytz.timezone(default_from_timezone)
60
+        datetime_object = from_tzinfo.localize(datetime_object)
61
+
62
+    new_tzinfo = pytz.timezone(to_timezone)
63
+    return datetime_object.astimezone(new_tzinfo)
64
+
65
+
31 66
 def date_time_in_long_format(datetime_object, format=''):
32 67
 
33 68
     current_locale = tg.i18n.get_lang()[0]
@@ -63,17 +98,6 @@ def current_year():
63 98
   now = datetime.datetime.now()
64 99
   return now.strftime('%Y')
65 100
 
66
-def formatLongDateAndTime(datetime_object, format=''):
67
-    """ OBSOLETE
68
-    :param datetime_object:
69
-    :param format:
70
-    :return:
71
-    """
72
-    if not format:
73
-        format = plag.Globals.LONG_DATE_FORMAT
74
-    return datetime_object.strftime(format)
75
-
76
-
77 101
 
78 102
 def icon(icon_name, white=False):
79 103
     if (white):

+ 10 - 1
tracim/tracim/lib/user.py View File

@@ -31,13 +31,22 @@ class UserApi(object):
31 31
     def get_one_by_id(self, id: int) -> User:
32 32
         return self._base_query().filter(User.user_id==id).one()
33 33
 
34
-    def update(self, user: User, name: str=None, email: str=None, do_save=True):
34
+    def update(
35
+            self,
36
+            user: User,
37
+            name: str=None,
38
+            email: str=None,
39
+            do_save=True,
40
+            timezone: str='',
41
+    ):
35 42
         if name is not None:
36 43
             user.display_name = name
37 44
 
38 45
         if email is not None:
39 46
             user.email = email
40 47
 
48
+        user.timezone = timezone
49
+
41 50
         if do_save:
42 51
             self.save(user)
43 52
 

+ 1 - 0
tracim/tracim/model/auth.py View File

@@ -124,6 +124,7 @@ class User(DeclarativeBase):
124 124
     created = Column(DateTime, default=datetime.utcnow)
125 125
     is_active = Column(Boolean, default=True, nullable=False)
126 126
     imported_from = Column(Unicode(32), nullable=True)
127
+    timezone = Column(Unicode(255), nullable=False, server_default='')
127 128
     _webdav_left_digest_response_hash = Column('webdav_left_digest_response_hash', Unicode(128))
128 129
     auth_token = Column(Unicode(255))
129 130
     auth_token_created = Column(DateTime)

+ 2 - 0
tracim/tracim/model/serializers.py View File

@@ -860,6 +860,7 @@ def serialize_user_list_default(user: User, context: Context):
860 860
     result['enabled'] = user.is_active
861 861
     result['profile'] = user.profile
862 862
     result['has_password'] = user.password!=None
863
+    result['timezone'] = user.timezone
863 864
     return result
864 865
 
865 866
 
@@ -882,6 +883,7 @@ def serialize_user_for_user(user: User, context: Context):
882 883
     result['enabled'] = user.is_active
883 884
     result['profile'] = user.profile
884 885
     result['calendar_url'] = user.calendar_url
886
+    result['timezone'] = user.timezone
885 887
 
886 888
     return result
887 889
 

+ 2 - 3
tracim/tracim/templates/admin/workspace_getone.mak View File

@@ -24,9 +24,8 @@
24 24
 <%def name="TITLE_ROW()">
25 25
     <div class="row-fluid">
26 26
         <div>
27
-            <%
28
-                subtitle = _('created on {}').format(h.formatLongDateAndTime(result.workspace.created))
29
-            %>
27
+            <% created_localized = h.get_with_timezone(result.workspace.created) %>
28
+            <% subtitle = _('workspace created on {date} at {time}').format(date=h.date(created_localized), time=h.time(created_localized)) %>
30 29
             ${ROW.TITLE_ROW(_('Workspace {}').format(result.workspace.label), 'fa-bank', 'col-md-offset-3 col-md-7', 't-user-color', subtitle)}
31 30
         </div>
32 31
     </div>

+ 4 - 2
tracim/tracim/templates/file/getone.mak View File

@@ -48,7 +48,8 @@
48 48
         </h1>
49 49
 
50 50
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
51
-          <p>${_('file created on {date} at {time} by <b>{author}</b>').format(date=h.date(result.file.created), time=h.time(result.file.created), author=result.file.owner.name)|n}</p>
51
+            <% created_localized = h.get_with_timezone(result.file.created) %>
52
+          <p>${_('file created on {date} at {time} by <b>{author}</b>').format(date=h.date(created_localized), time=h.time(created_localized), author=result.file.owner.name)|n}</p>
52 53
         </div>
53 54
     </div>
54 55
 </div>
@@ -107,7 +108,8 @@
107 108
                 </tr>
108 109
                 <tr>
109 110
                     <td class="tracim-title">${_('Modified')}</td>
110
-                    <td>${h.format_short(result.file.created)|n} ${_('by {}').format(result.file.owner.name)}</td>
111
+                    <% created_localized = h.get_with_timezone(result.file.created) %>
112
+                    <td>${h.format_short(created_localized)|n} ${_('by {}').format(result.file.owner.name)}</td>
111 113
                 </tr>
112 114
             </table>
113 115
         </div>

+ 2 - 1
tracim/tracim/templates/folder/getone.mak View File

@@ -46,7 +46,8 @@
46 46
         </h1>
47 47
 
48 48
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
49
-          <p>${_('folder created on {date} at {time} by <b>{author}</b>').format(date=h.date(result.folder.created), time=h.time(result.folder.created), author=result.folder.owner.name)|n}</p>
49
+            <% created_localized = h.get_with_timezone(result.folder.created) %>
50
+          <p>${_('folder created on {date} at {time} by <b>{author}</b>').format(date=h.date(created_localized), time=h.time(created_localized), author=result.folder.owner.name)|n}</p>
50 51
         </div>
51 52
     </div>
52 53
 </div>

+ 2 - 1
tracim/tracim/templates/page/getone.mak View File

@@ -47,7 +47,8 @@
47 47
         </h1>
48 48
 
49 49
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
50
-          <p>${_('page created on {date} at {time} by <b>{author}</b>').format(date=h.date(result.page.created), time=h.time(result.page.created), author=result.page.owner.name)|n}</p>
50
+            <% created_localized = h.get_with_timezone(result.page.created) %>
51
+          <p>${_('page created on {date} at {time} by <b>{author}</b>').format(date=h.date(created_localized), time=h.time(created_localized), author=result.page.owner.name)|n}</p>
51 52
         </div>
52 53
     </div>
53 54
 </div>

+ 2 - 1
tracim/tracim/templates/search/display.mak View File

@@ -42,7 +42,8 @@
42 42
         </h1>
43 43
 
44 44
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
45
-##          <p>${_('folder created on {date} at {time} by <b>{author}</b>').format(date=h.date(result.folder.created), time=h.time(result.folder.created), author=result.folder.owner.name)|n}</p>
45
+            <% created_localized = h.get_with_timezone(result.folder.created) %>
46
+##          <p>${_('folder created on {date} at {time} by <b>{author}</b>').format(date=h.date(created_localized), time=h.time(created_localized), author=result.folder.owner.name)|n}</p>
46 47
         </div>
47 48
     </div>
48 49
 </div>

+ 2 - 1
tracim/tracim/templates/thread/getone.mak View File

@@ -48,7 +48,8 @@
48 48
         </h1>
49 49
 
50 50
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
51
-          <p>${_('page created on {date} at {time} by <b>{author}</b>').format(date=h.date(result.thread.created), time=h.time(result.thread.created), author=result.thread.owner.name)|n}</p>
51
+            <% created_localized = h.get_with_timezone(result.thread.created) %>
52
+          <p>${_('page created on {date} at {time} by <b>{author}</b>').format(date=h.date(created_localized), time=h.time(created_localized), author=result.thread.owner.name)|n}</p>
52 53
         </div>
53 54
     </div>
54 55
 </div>

+ 14 - 0
tracim/tracim/templates/user_workspace_forms.mak View File

@@ -115,6 +115,20 @@
115 115
                 <span class="info readonly">${_('This calendar URL will work with CalDav compatibles clients')}</span>
116 116
                 <input id="calendar" type="text" class="form-control"  disabled="disabled" value="${user.calendar_url}" />
117 117
             </div>
118
+            <div class="form-group">
119
+                <label for="timezone">${_('Timezone')}</label>
120
+                <span class="info readonly">${_('Dates will be displayed with this timezone')}</span>
121
+                <select id="timezone" name="timezone" class="form-control">
122
+                    <option value=""></option>
123
+                    % for timezone in timezones:
124
+                        % if timezone == user.timezone:
125
+                            <option value="${timezone}" selected>${timezone}</option>
126
+                        % else:
127
+                            <option value="${timezone}">${timezone}</option>
128
+                        % endif
129
+                    % endfor
130
+                </select>
131
+            </div>
118 132
         </div>
119 133
         <div class="modal-footer">
120 134
             <span class="pull-right t-modal-form-submit-button">

+ 6 - 3
tracim/tracim/templates/user_workspace_widgets.mak View File

@@ -303,6 +303,7 @@
303 303
 </%def>
304 304
 
305 305
 <%def name="SECURED_TIMELINE_ITEM(user, item)">
306
+##     <% created_localized = h.get_with_timezone(item.created) %>
306 307
 ##     <div class="row t-odd-or-even t-hacky-thread-comment-border-top">
307 308
 ##         <div class="col-sm-7 col-sm-offset-3">
308 309
 ##             <div class="t-timeline-item">
@@ -312,7 +313,7 @@
312 313
 ##                 <h5 style="margin: 0;">
313 314
 ##                     <span class="tracim-less-visible">${_('<strong>{}</strong> wrote:').format(item.owner.name)|n}</span>
314 315
 ##
315
-##                     <div class="pull-right text-right t-timeline-item-moment" title="${h.date_time(item.created)|n}">
316
+##                     <div class="pull-right text-right t-timeline-item-moment" title="${h.date_time(created_localized)|n}">
316 317
 ##                         ${_('{delta} ago').format(delta=item.created_as_delta)}
317 318
 ##
318 319
 ##                         % if h.is_item_still_editable(item) and item.owner.id==user.id:
@@ -336,6 +337,7 @@
336 337
 </%def>
337 338
 
338 339
 <%def name="SECURED_HISTORY_VIRTUAL_EVENT(user, event)">
340
+    <% created_localized = h.get_with_timezone(event.created) %>
339 341
     <% is_new_css_class = 't-is-new-content' if event.is_new else '' %>
340 342
 
341 343
     <div class="row t-odd-or-even t-hacky-thread-comment-border-top ${is_new_css_class}">
@@ -353,7 +355,7 @@
353 355
                         <span class="tracim-less-visible">${_('{} by <strong>{}</strong>').format(event.label, event.owner.name)|n}</span>
354 356
                     % endif
355 357
 
356
-                    <div class="pull-right text-right t-timeline-item-moment" title="${h.date_time(event.created)|n}">
358
+                    <div class="pull-right text-right t-timeline-item-moment" title="${h.date_time(created_localized)|n}">
357 359
                         ${_('{delta} ago').format(delta=event.created_as_delta)}
358 360
 
359 361
                         % if h.is_item_still_editable(CFG, event) and event.owner.id==user.id:
@@ -374,6 +376,7 @@
374 376
 </%def>
375 377
 
376 378
 <%def name="SECURED_HISTORY_VIRTUAL_EVENT_AS_TABLE_ROW(user, event, current_revision_id)">
379
+    <% created_localized = h.get_with_timezone(event.created) %>
377 380
     <%
378 381
         warning_or_not = ('', 'warning')[current_revision_id==event.id]
379 382
         row_css = 't-is-new-content' if event.is_new else warning_or_not
@@ -382,7 +385,7 @@
382 385
         <td class="t-less-visible">
383 386
             <span class="label label-default">${ICON.FA_FW(event.type.icon)} ${event.type.label}</span>
384 387
         </td>
385
-        <td title="${h.date_time(event.created)|n}">${_('{delta} ago').format(delta=event.created_as_delta)}</td>
388
+        <td title="${h.date_time(created_localized)|n}">${_('{delta} ago').format(delta=event.created_as_delta)}</td>
386 389
         <td>${event.owner.name}</td>
387 390
 ## FIXME - REMOVE                            <td>${event}</td>
388 391
 

+ 2 - 1
tracim/tracim/templates/workspace/getone.mak View File

@@ -44,7 +44,8 @@
44 44
         </h1>
45 45
 
46 46
         <div style="margin: -1.5em auto -1.5em auto;" class="t-less-visible">
47
-          <p>${_('workspace created on {date} at {time}').format(date=h.date(result.workspace.created), time=h.time(result.workspace.created))|n}</p>
47
+            <% created_localized = h.get_with_timezone(result.workspace.created) %>
48
+          <p>${_('workspace created on {date} at {time}').format(date=h.date(created_localized), time=h.time(created_localized))|n}</p>
48 49
         </div>
49 50
     </div>
50 51
 </div>

+ 2 - 1
tracim/tracim/tests/__init__.py View File

@@ -51,7 +51,8 @@ class TestApp(BaseTestApp):
51 51
         try:
52 52
             super()._check_status(status, res)
53 53
         except AppError as exc:
54
-            dump_file_path = "/tmp/debug_%d_%s.html" % (time.time() * 1000, res.request.path_qs[1:])
54
+            escaped_page_name = res.request.path_qs[1:].replace('/', '')
55
+            dump_file_path = "/tmp/debug_%d_%s.html" % (time.time() * 1000, escaped_page_name)
55 56
             if os.path.exists("/tmp"):
56 57
                 with open(dump_file_path, 'w') as dump_file:
57 58
                     print(res.ubody, file=dump_file)

+ 1 - 0
tracim/tracim/tests/functional/test_ldap_restrictions.py View File

@@ -68,6 +68,7 @@ class TestAuthentication(LDAPTest, TracimTestController):
68 68
             OrderedDict([
69 69
                 ('name', 'Lawrence Lessig YEAH'),
70 70
                 ('email', 'An-other-email@fsf.org'),
71
+                ('timezone', ''),
71 72
             ])
72 73
         )
73 74
 

+ 21 - 8
tracim/tracim/tests/library/test_helpers.py View File

@@ -1,20 +1,18 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 
3 3
 import datetime
4
+from unittest.mock import MagicMock
4 5
 
6
+import pytz
7
+from babel.dates import get_timezone
5 8
 from nose.tools import eq_
6
-from nose.tools import ok_
9
+from tg.request_local import TurboGearsContextMember
10
+from tg.util.webtest import test_context
11
+from tg import tmpl_context
7 12
 
8 13
 import tracim.lib.helpers as h
9 14
 from tracim.config.app_cfg import CFG
10
-from tracim.model.data import Content
11
-from tracim.model.data import ContentType
12
-from tracim.model.data import Workspace
13
-
14
-from tracim.model.serializers import Context
15
-from tracim.model.serializers import CTX
16 15
 from tracim.model.serializers import DictLikeClass
17
-
18 16
 from tracim.tests import TestStandard
19 17
 
20 18
 
@@ -45,3 +43,18 @@ class TestHelpers(TestStandard):
45 43
         config.DATA_UPDATE_ALLOWED_DURATION = 8
46 44
         item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
47 45
         eq_(False, h.is_item_still_editable(config, item))
46
+
47
+    def test_unit__change_datetime_timezone__ok__with_naive_and_current_user(self):  # nopep8
48
+        user_mock = MagicMock(timezone='America/Guadeloupe')
49
+
50
+        with test_context(self.app):
51
+            tmpl_context.current_user = user_mock
52
+            naive_datetime = datetime.datetime(2000, 1, 1, 0, 0, 0)
53
+
54
+            new_datetime = h.get_with_timezone(
55
+                datetime_object=naive_datetime,
56
+                default_from_timezone='UTC',
57
+                to_timezone='',  # user_mock.timezone should be used
58
+            )
59
+
60
+            eq_(str(new_datetime), '1999-12-31 20:00:00-04:00')