Repository: ckan/ckan

Issue:
Activity timestamps are stored in local time instead of UTC
https://github.com/ckan/ckan/blob/master/ckan/model/activity.py#L41

If server has local time set to some timezone that differs from UTC, activity streams show -1 days ago instead of just now, since datetime.now() return local time instead of UTC. There probably are others as well which use local time instead of UTC, at least based on a quick search https://github.com/ckan/ckan/search?utf8=%E2%9C%93&q=datetime.now%28%29&type=Code



Discussion:
That's not good.

A fix would include a migration that updates the times in the db (using the current time offset should be close enough for the activity stream) and updating the templates to use the snippet that adjusts the display to the client's time zone

@wardi and apparently "-1 days ago" is returned from here https://github.com/ckan/ckan/blob/master/ckan/lib/formatters.py#L71 if server time is in UTC but ckan.display_timezone is something else like Europe/Helsinki

I confirm that activity timestamps are always stored in server local timezone guessing it is UTC.

Given server timezone is `America/Sao_Paulo` (-03:00). When `ckan.display_timezone` is `server` then a difference of -6 hours is shown. When `ckan.display_timezone` is `UTC` then the difference becomes -3 hours (still incorrect).

According to this ticket the issue may be more than 4 years old: http://trac.ckan.org/ticket/331.html


Resolving patch:
diff --git a/ckan/lib/formatters.py b/ckan/lib/formatters.py
--- a/ckan/lib/formatters.py
+++ b/ckan/lib/formatters.py
@@ -99,12 +99,10 @@ def months_between(date1, date2):
         return months
 
     if not show_date:
-        now = datetime.datetime.utcnow()
-        if datetime_.tzinfo is not None:
-            now = now.replace(tzinfo=datetime_.tzinfo)
-        else:
-            now = now.replace(tzinfo=pytz.utc)
+        now = datetime.datetime.now(pytz.utc)
+        if datetime_.tzinfo is None:
             datetime_ = datetime_.replace(tzinfo=pytz.utc)
+
         date_diff = now - datetime_
         days = date_diff.days
         if days < 1 and now > datetime_:
diff --git a/ckan/migration/versions/085_adjust_activity_timestamps.py b/ckan/migration/versions/085_adjust_activity_timestamps.py
new file mode 100644
--- /dev/null
+++ b/ckan/migration/versions/085_adjust_activity_timestamps.py
@@ -0,0 +1,16 @@
+# encoding: utf-8
+
+import datetime
+
+
+def upgrade(migrate_engine):
+    u"""
+    The script assumes that the current timestamp was
+    recorded with the server's current set timezone
+    """
+    utc_now = datetime.datetime.utcnow()
+    local_now = datetime.datetime.now()
+
+    with migrate_engine.begin() as connection:
+        sql = u"update activity set timestamp = timestamp + (%s - %s);"
+        connection.execute(sql, utc_now, local_now)
diff --git a/ckan/model/activity.py b/ckan/model/activity.py
--- a/ckan/model/activity.py
+++ b/ckan/model/activity.py
@@ -40,7 +40,7 @@ class Activity(domain_object.DomainObject):
     def __init__(self, user_id, object_id, revision_id, activity_type,
             data=None):
         self.id = _types.make_uuid()
-        self.timestamp = datetime.datetime.now()
+        self.timestamp = datetime.datetime.utcnow()
         self.user_id = user_id
         self.object_id = object_id
         self.revision_id = revision_id


Test patch: