Browse Source

Closes #118: Allow user to specify his timezone

Bastien Sevajol (Algoo) 8 years ago
parent
commit
4ce44b83a4

+ 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
 
4 5
 from tracim import model  as pm
@@ -174,10 +175,14 @@ class UserRestController(TIMRestController):
174 175
 
175 176
         dictified_user = Context(CTX.USER).toDict(current_user, 'user')
176 177
         fake_api = DictLikeClass(next_url=next_url)
177
-        return DictLikeClass(result=dictified_user, fake_api=fake_api)
178
+        return DictLikeClass(
179
+            result=dictified_user,
180
+            fake_api=fake_api,
181
+            timezones=pytz.all_timezones,
182
+        )
178 183
 
179 184
     @tg.expose('tracim.templates.workspace.edit')
180
-    def put(self, user_id, name, email, next_url=None):
185
+    def put(self, user_id, name, email, timezone, next_url=None):
181 186
         user_id = tmpl_context.current_user.user_id
182 187
         current_user = tmpl_context.current_user
183 188
         assert user_id==current_user.user_id
@@ -185,7 +190,8 @@ class UserRestController(TIMRestController):
185 190
         # Only keep allowed field update
186 191
         updated_fields = self._clean_update_fields({
187 192
             'name': name,
188
-            'email': email
193
+            'email': email,
194
+            'timezone': timezone,
189 195
         })
190 196
 
191 197
         api = UserApi(tmpl_context.current_user)

+ 37 - 2
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]

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

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

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

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

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

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

+ 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 = _('created on {}'.format(h.formatLongDateAndTime(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>

+ 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')