浏览代码

Closes #118: Allow user to specify his timezone

Bastien Sevajol (Algoo) 8 年前
父节点
当前提交
4ce44b83a4

+ 26 - 0
tracim/migration/versions/2cd20ff3d23a_user_timezone.py 查看文件

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 查看文件

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+import pytz
2
 from webob.exc import HTTPForbidden
3
 from webob.exc import HTTPForbidden
3
 
4
 
4
 from tracim import model  as pm
5
 from tracim import model  as pm
174
 
175
 
175
         dictified_user = Context(CTX.USER).toDict(current_user, 'user')
176
         dictified_user = Context(CTX.USER).toDict(current_user, 'user')
176
         fake_api = DictLikeClass(next_url=next_url)
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
     @tg.expose('tracim.templates.workspace.edit')
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
         user_id = tmpl_context.current_user.user_id
186
         user_id = tmpl_context.current_user.user_id
182
         current_user = tmpl_context.current_user
187
         current_user = tmpl_context.current_user
183
         assert user_id==current_user.user_id
188
         assert user_id==current_user.user_id
185
         # Only keep allowed field update
190
         # Only keep allowed field update
186
         updated_fields = self._clean_update_fields({
191
         updated_fields = self._clean_update_fields({
187
             'name': name,
192
             'name': name,
188
-            'email': email
193
+            'email': email,
194
+            'timezone': timezone,
189
         })
195
         })
190
 
196
 
191
         api = UserApi(tmpl_context.current_user)
197
         api = UserApi(tmpl_context.current_user)

+ 37 - 2
tracim/tracim/lib/helpers.py 查看文件

6
 
6
 
7
 import datetime
7
 import datetime
8
 
8
 
9
+import pytz
9
 import slugify
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
 from markupsafe import Markup
13
 from markupsafe import Markup
12
 
14
 
13
 import tg
15
 import tg
16
+from tg import tmpl_context
14
 from tg.i18n import ugettext as _
17
 from tg.i18n import ugettext as _
15
 
18
 
16
 from tracim.lib import app_globals as plag
19
 from tracim.lib import app_globals as plag
20
 from tracim.lib.content import ContentApi
23
 from tracim.lib.content import ContentApi
21
 from tracim.lib.userworkspace import RoleApi
24
 from tracim.lib.userworkspace import RoleApi
22
 from tracim.lib.workspace import WorkspaceApi
25
 from tracim.lib.workspace import WorkspaceApi
23
-from tracim.model import User
24
 
26
 
25
 from tracim.model.data import ContentStatus
27
 from tracim.model.data import ContentStatus
26
 from tracim.model.data import Content
28
 from tracim.model.data import Content
28
 from tracim.model.data import UserRoleInWorkspace
30
 from tracim.model.data import UserRoleInWorkspace
29
 from tracim.model.data import Workspace
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
 def date_time_in_long_format(datetime_object, format=''):
66
 def date_time_in_long_format(datetime_object, format=''):
32
 
67
 
33
     current_locale = tg.i18n.get_lang()[0]
68
     current_locale = tg.i18n.get_lang()[0]

+ 10 - 1
tracim/tracim/lib/user.py 查看文件

30
     def get_one_by_id(self, id: int) -> User:
30
     def get_one_by_id(self, id: int) -> User:
31
         return self._base_query().filter(User.user_id==id).one()
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
         if name is not None:
41
         if name is not None:
35
             user.display_name = name
42
             user.display_name = name
36
 
43
 
37
         if email is not None:
44
         if email is not None:
38
             user.email = email
45
             user.email = email
39
 
46
 
47
+        user.timezone = timezone
48
+
40
         if do_save:
49
         if do_save:
41
             self.save(user)
50
             self.save(user)
42
 
51
 

+ 1 - 0
tracim/tracim/model/auth.py 查看文件

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

+ 2 - 0
tracim/tracim/model/serializers.py 查看文件

858
     result['enabled'] = user.is_active
858
     result['enabled'] = user.is_active
859
     result['profile'] = user.profile
859
     result['profile'] = user.profile
860
     result['has_password'] = user.password!=None
860
     result['has_password'] = user.password!=None
861
+    result['timezone'] = user.timezone
861
     return result
862
     return result
862
 
863
 
863
 
864
 
880
     result['enabled'] = user.is_active
881
     result['enabled'] = user.is_active
881
     result['profile'] = user.profile
882
     result['profile'] = user.profile
882
     result['calendar_url'] = user.calendar_url
883
     result['calendar_url'] = user.calendar_url
884
+    result['timezone'] = user.timezone
883
 
885
 
884
     return result
886
     return result
885
 
887
 

+ 2 - 3
tracim/tracim/templates/admin/workspace_getone.mak 查看文件

24
 <%def name="TITLE_ROW()">
24
 <%def name="TITLE_ROW()">
25
     <div class="row-fluid">
25
     <div class="row-fluid">
26
         <div>
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
             ${ROW.TITLE_ROW(_('Workspace {}').format(result.workspace.label), 'fa-bank', 'col-md-offset-3 col-md-7', 't-user-color', subtitle)}
29
             ${ROW.TITLE_ROW(_('Workspace {}').format(result.workspace.label), 'fa-bank', 'col-md-offset-3 col-md-7', 't-user-color', subtitle)}
31
         </div>
30
         </div>
32
     </div>
31
     </div>

+ 4 - 2
tracim/tracim/templates/file/getone.mak 查看文件

48
         </h1>
48
         </h1>
49
 
49
 
50
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
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
         </div>
53
         </div>
53
     </div>
54
     </div>
54
 </div>
55
 </div>
107
                 </tr>
108
                 </tr>
108
                 <tr>
109
                 <tr>
109
                     <td class="tracim-title">${_('Modified')}</td>
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
                 </tr>
113
                 </tr>
112
             </table>
114
             </table>
113
         </div>
115
         </div>

+ 2 - 1
tracim/tracim/templates/folder/getone.mak 查看文件

46
         </h1>
46
         </h1>
47
 
47
 
48
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
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
         </div>
51
         </div>
51
     </div>
52
     </div>
52
 </div>
53
 </div>

+ 2 - 1
tracim/tracim/templates/page/getone.mak 查看文件

47
         </h1>
47
         </h1>
48
 
48
 
49
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
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
         </div>
52
         </div>
52
     </div>
53
     </div>
53
 </div>
54
 </div>

+ 2 - 1
tracim/tracim/templates/search/display.mak 查看文件

42
         </h1>
42
         </h1>
43
 
43
 
44
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
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
         </div>
47
         </div>
47
     </div>
48
     </div>
48
 </div>
49
 </div>

+ 2 - 1
tracim/tracim/templates/thread/getone.mak 查看文件

48
         </h1>
48
         </h1>
49
 
49
 
50
         <div style="margin: -1.5em auto -1.5em auto;" class="tracim-less-visible">
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
         </div>
53
         </div>
53
     </div>
54
     </div>
54
 </div>
55
 </div>

+ 14 - 0
tracim/tracim/templates/user_workspace_forms.mak 查看文件

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
                 <input id="calendar" type="text" class="form-control"  disabled="disabled" value="${user.calendar_url}" />
117
             </div>
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
         </div>
132
         </div>
119
         <div class="modal-footer">
133
         <div class="modal-footer">
120
             <span class="pull-right t-modal-form-submit-button">
134
             <span class="pull-right t-modal-form-submit-button">

+ 6 - 3
tracim/tracim/templates/user_workspace_widgets.mak 查看文件

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

+ 2 - 1
tracim/tracim/templates/workspace/getone.mak 查看文件

44
         </h1>
44
         </h1>
45
 
45
 
46
         <div style="margin: -1.5em auto -1.5em auto;" class="t-less-visible">
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
         </div>
49
         </div>
49
     </div>
50
     </div>
50
 </div>
51
 </div>

+ 21 - 8
tracim/tracim/tests/library/test_helpers.py 查看文件

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 
2
 
3
 import datetime
3
 import datetime
4
+from unittest.mock import MagicMock
4
 
5
 
6
+import pytz
7
+from babel.dates import get_timezone
5
 from nose.tools import eq_
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
 import tracim.lib.helpers as h
13
 import tracim.lib.helpers as h
9
 from tracim.config.app_cfg import CFG
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
 from tracim.model.serializers import DictLikeClass
15
 from tracim.model.serializers import DictLikeClass
17
-
18
 from tracim.tests import TestStandard
16
 from tracim.tests import TestStandard
19
 
17
 
20
 
18
 
45
         config.DATA_UPDATE_ALLOWED_DURATION = 8
43
         config.DATA_UPDATE_ALLOWED_DURATION = 8
46
         item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
44
         item.created = datetime.datetime.now() - datetime.timedelta(0, 10)
47
         eq_(False, h.is_item_still_editable(config, item))
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')