#3030 Use timezone not offset for user activity, fix heat map
Merged 6 years ago by pingou. Opened 6 years ago by adamwill.
adamwill/pagure more-timezone-fun  into  master

file modified
+19 -7
@@ -481,7 +481,7 @@ 

  

      """

      date_format = flask.request.args.get('format', 'isoformat')

-     offset = flask.request.args.get('offset', 0)

+     tz = flask.request.args.get('tz', 'UTC')

  

      user = _get_user(username=username)

  
@@ -489,17 +489,29 @@ 

          flask.g.session,

          user,

          datetime.datetime.utcnow().date() + datetime.timedelta(days=1),

-         offset=offset

+         tz=tz

      )

  

-     def format_date(d):

+     def format_date(d, tz):

          if date_format == 'timestamp':

-             d = d.strftime('%s')

+             # the reason we have this at all is the cal-heatmap js lib

+             # wants times as timestamps. We're trying to feed it a

+             # timestamp it will count as having happened on date 'd'.

+             # However, cal-heatmap always uses the browser timezone,

+             # so we have to be careful to produce a timestamp which

+             # falls on the correct date *in the browser timezone*. We

+             # aim for noon on the desired date.

+             try:

+ 

+                 return arrow.get(d, tz).replace(hour=12).timestamp

+             except arrow.parser.ParserError:

+                 # if tz is invalid for some reason, just go with UTC

+                 return arrow.get(d).replace(hour=12).timestamp

          else:

              d = d.isoformat()

          return d

  

-     stats = {format_date(d[0]): d[1] for d in stats}

+     stats = {format_date(d[0], tz): d[1] for d in stats}

  

      jsonout = flask.jsonify(stats)

      return jsonout
@@ -578,7 +590,7 @@ 

  

      """  # noqa

      grouped = str(flask.request.args.get('grouped')).lower() in ['1', 'true']

-     offset = flask.request.args.get('offset', 0)

+     tz = flask.request.args.get('tz', 'UTC')

  

      try:

          date = arrow.get(date)
@@ -590,7 +602,7 @@ 

      user = _get_user(username=username)

  

      activities = pagure.lib.get_user_activity_day(

-         flask.g.session, user, date, offset=offset

+         flask.g.session, user, date, tz=tz

      )

      js_act = []

      if grouped:

file modified
+4 -4
@@ -4352,7 +4352,7 @@ 

          return 'Custom field %s reset (from %s)' % (key.name, old_value)

  

  

- def get_yearly_stats_user(session, user, date, offset=0):

+ def get_yearly_stats_user(session, user, date, tz='UTC'):

      """ Return the activity of the specified user in the year preceding the

      specified date. 'offset' is intended to be a timezone offset from UTC,

      in minutes: you can discover the offset for a timezone and pass that
@@ -4376,10 +4376,10 @@ 

      # us a dict with the dates as keys and the number of times each

      # date occurs in the data as the values, we return its items as

      # a list of tuples

-     return Counter([event.date_offset(offset) for event in events]).items()

+     return Counter([event.date_tz(tz) for event in events]).items()

  

  

- def get_user_activity_day(session, user, date, offset=0):

+ def get_user_activity_day(session, user, date, tz='UTC'):

      """ Return the activity of the specified user on the specified date.

      'offset' is intended to be a timezone offset from UTC, in minutes:

      you can discover the offset for a timezone and pass that, so this
@@ -4414,7 +4414,7 @@ 

      events = query.all()

      # Now we filter down to the events that *really* occurred on the

      # date we were asked for with the offset applied, and return

-     return [ev for ev in events if ev.date_offset(offset) == dt.date()]

+     return [ev for ev in events if ev.date_tz(tz) == dt.date()]

  

  

  def log_action(session, action, obj, user_obj):

file modified
+10 -7
@@ -11,6 +11,7 @@ 

  __requires__ = ['SQLAlchemy >= 0.8', 'jinja2 >= 2.4']  # noqa

  import pkg_resources  # noqa: E402,F401

  

+ import arrow

  import datetime

  import collections

  import logging
@@ -2527,15 +2528,17 @@ 

  

          return desc % arg

  

-     def date_offset(self, offset):

+     def date_tz(self, tz='UTC'):

          '''Returns the date (as a datetime.date()) of this log entry

-         with a specified offset (in minutes) applied. Necessary if we

-         want to know what date this event occurred on in a particular

-         time zone.

+         in a specified timezone (Olson name as a string). Assumes that

+         date_created is aware, or UTC. If tz isn't a valid timezone

+         identifier for arrow, just returns the date component of

+         date_created.

          '''

-         offsetdt = self.date_created + datetime.timedelta(minutes=int(offset))

-         return offsetdt.date()

- 

+         try:

+             return arrow.get(self.date_created).to(tz).date()

+         except arrow.parser.ParserError:

+             return self.date_created.date()

  

  class IssueWatcher(BASE):

      """ Stores the users watching issues.

The added file is too large to be shown here, see it at: pagure/static/vendor/jstimezonedetect/jstz-1.0.6.js
@@ -0,0 +1,2 @@ 

+ /* jstz.min.js Version: 1.0.6 Build date: 2015-11-04 */

+ !function(e){var a=function(){"use strict";var e="s",s={DAY:864e5,HOUR:36e5,MINUTE:6e4,SECOND:1e3,BASELINE_YEAR:2014,MAX_SCORE:864e6,AMBIGUITIES:{"America/Denver":["America/Mazatlan"],"Europe/London":["Africa/Casablanca"],"America/Chicago":["America/Mexico_City"],"America/Asuncion":["America/Campo_Grande","America/Santiago"],"America/Montevideo":["America/Sao_Paulo","America/Santiago"],"Asia/Beirut":["Asia/Amman","Asia/Jerusalem","Europe/Helsinki","Asia/Damascus","Africa/Cairo","Asia/Gaza","Europe/Minsk"],"Pacific/Auckland":["Pacific/Fiji"],"America/Los_Angeles":["America/Santa_Isabel"],"America/New_York":["America/Havana"],"America/Halifax":["America/Goose_Bay"],"America/Godthab":["America/Miquelon"],"Asia/Dubai":["Asia/Yerevan"],"Asia/Jakarta":["Asia/Krasnoyarsk"],"Asia/Shanghai":["Asia/Irkutsk","Australia/Perth"],"Australia/Sydney":["Australia/Lord_Howe"],"Asia/Tokyo":["Asia/Yakutsk"],"Asia/Dhaka":["Asia/Omsk"],"Asia/Baku":["Asia/Yerevan"],"Australia/Brisbane":["Asia/Vladivostok"],"Pacific/Noumea":["Asia/Vladivostok"],"Pacific/Majuro":["Asia/Kamchatka","Pacific/Fiji"],"Pacific/Tongatapu":["Pacific/Apia"],"Asia/Baghdad":["Europe/Minsk","Europe/Moscow"],"Asia/Karachi":["Asia/Yekaterinburg"],"Africa/Johannesburg":["Asia/Gaza","Africa/Cairo"]}},i=function(e){var a=-e.getTimezoneOffset();return null!==a?a:0},r=function(){var a=i(new Date(s.BASELINE_YEAR,0,2)),r=i(new Date(s.BASELINE_YEAR,5,2)),n=a-r;return 0>n?a+",1":n>0?r+",1,"+e:a+",0"},n=function(){var e,a;if("undefined"!=typeof Intl&&"undefined"!=typeof Intl.DateTimeFormat&&(e=Intl.DateTimeFormat(),"undefined"!=typeof e&&"undefined"!=typeof e.resolvedOptions))return a=e.resolvedOptions().timeZone,a&&(a.indexOf("/")>-1||"UTC"===a)?a:void 0},o=function(e){for(var a=new Date(e,0,1,0,0,1,0).getTime(),s=new Date(e,12,31,23,59,59).getTime(),i=a,r=new Date(i).getTimezoneOffset(),n=null,o=null;s-864e5>i;){var t=new Date(i),A=t.getTimezoneOffset();A!==r&&(r>A&&(n=t),A>r&&(o=t),r=A),i+=864e5}return n&&o?{s:u(n).getTime(),e:u(o).getTime()}:!1},u=function l(e,a,i){"undefined"==typeof a&&(a=s.DAY,i=s.HOUR);for(var r=new Date(e.getTime()-a).getTime(),n=e.getTime()+a,o=new Date(r).getTimezoneOffset(),u=r,t=null;n-i>u;){var A=new Date(u),c=A.getTimezoneOffset();if(c!==o){t=A;break}u+=i}return a===s.DAY?l(t,s.HOUR,s.MINUTE):a===s.HOUR?l(t,s.MINUTE,s.SECOND):t},t=function(e,a,s,i){if("N/A"!==s)return s;if("Asia/Beirut"===a){if("Africa/Cairo"===i.name&&13983768e5===e[6].s&&14116788e5===e[6].e)return 0;if("Asia/Jerusalem"===i.name&&13959648e5===e[6].s&&14118588e5===e[6].e)return 0}else if("America/Santiago"===a){if("America/Asuncion"===i.name&&14124816e5===e[6].s&&1397358e6===e[6].e)return 0;if("America/Campo_Grande"===i.name&&14136912e5===e[6].s&&13925196e5===e[6].e)return 0}else if("America/Montevideo"===a){if("America/Sao_Paulo"===i.name&&14136876e5===e[6].s&&1392516e6===e[6].e)return 0}else if("Pacific/Auckland"===a&&"Pacific/Fiji"===i.name&&14142456e5===e[6].s&&13961016e5===e[6].e)return 0;return s},A=function(e,i){for(var r=function(a){for(var r=0,n=0;n<e.length;n++)if(a.rules[n]&&e[n]){if(!(e[n].s>=a.rules[n].s&&e[n].e<=a.rules[n].e)){r="N/A";break}if(r=0,r+=Math.abs(e[n].s-a.rules[n].s),r+=Math.abs(a.rules[n].e-e[n].e),r>s.MAX_SCORE){r="N/A";break}}return r=t(e,i,r,a)},n={},o=a.olson.dst_rules.zones,u=o.length,A=s.AMBIGUITIES[i],c=0;u>c;c++){var m=o[c],l=r(o[c]);"N/A"!==l&&(n[m.name]=l)}for(var f in n)if(n.hasOwnProperty(f))for(var d=0;d<A.length;d++)if(A[d]===f)return f;return i},c=function(e){var s=function(){for(var e=[],s=0;s<a.olson.dst_rules.years.length;s++){var i=o(a.olson.dst_rules.years[s]);e.push(i)}return e},i=function(e){for(var a=0;a<e.length;a++)if(e[a]!==!1)return!0;return!1},r=s(),n=i(r);return n?A(r,e):e},m=function(){var e=n();return e||(e=a.olson.timezones[r()],"undefined"!=typeof s.AMBIGUITIES[e]&&(e=c(e))),{name:function(){return e}}};return{determine:m}}();a.olson=a.olson||{},a.olson.timezones={"-720,0":"Etc/GMT+12","-660,0":"Pacific/Pago_Pago","-660,1,s":"Pacific/Apia","-600,1":"America/Adak","-600,0":"Pacific/Honolulu","-570,0":"Pacific/Marquesas","-540,0":"Pacific/Gambier","-540,1":"America/Anchorage","-480,1":"America/Los_Angeles","-480,0":"Pacific/Pitcairn","-420,0":"America/Phoenix","-420,1":"America/Denver","-360,0":"America/Guatemala","-360,1":"America/Chicago","-360,1,s":"Pacific/Easter","-300,0":"America/Bogota","-300,1":"America/New_York","-270,0":"America/Caracas","-240,1":"America/Halifax","-240,0":"America/Santo_Domingo","-240,1,s":"America/Asuncion","-210,1":"America/St_Johns","-180,1":"America/Godthab","-180,0":"America/Argentina/Buenos_Aires","-180,1,s":"America/Montevideo","-120,0":"America/Noronha","-120,1":"America/Noronha","-60,1":"Atlantic/Azores","-60,0":"Atlantic/Cape_Verde","0,0":"UTC","0,1":"Europe/London","60,1":"Europe/Berlin","60,0":"Africa/Lagos","60,1,s":"Africa/Windhoek","120,1":"Asia/Beirut","120,0":"Africa/Johannesburg","180,0":"Asia/Baghdad","180,1":"Europe/Moscow","210,1":"Asia/Tehran","240,0":"Asia/Dubai","240,1":"Asia/Baku","270,0":"Asia/Kabul","300,1":"Asia/Yekaterinburg","300,0":"Asia/Karachi","330,0":"Asia/Kolkata","345,0":"Asia/Kathmandu","360,0":"Asia/Dhaka","360,1":"Asia/Omsk","390,0":"Asia/Rangoon","420,1":"Asia/Krasnoyarsk","420,0":"Asia/Jakarta","480,0":"Asia/Shanghai","480,1":"Asia/Irkutsk","525,0":"Australia/Eucla","525,1,s":"Australia/Eucla","540,1":"Asia/Yakutsk","540,0":"Asia/Tokyo","570,0":"Australia/Darwin","570,1,s":"Australia/Adelaide","600,0":"Australia/Brisbane","600,1":"Asia/Vladivostok","600,1,s":"Australia/Sydney","630,1,s":"Australia/Lord_Howe","660,1":"Asia/Kamchatka","660,0":"Pacific/Noumea","690,0":"Pacific/Norfolk","720,1,s":"Pacific/Auckland","720,0":"Pacific/Majuro","765,1,s":"Pacific/Chatham","780,0":"Pacific/Tongatapu","780,1,s":"Pacific/Apia","840,0":"Pacific/Kiritimati"},a.olson.dst_rules={years:[2008,2009,2010,2011,2012,2013,2014],zones:[{name:"Africa/Cairo",rules:[{e:12199572e5,s:12090744e5},{e:1250802e6,s:1240524e6},{e:12858804e5,s:12840696e5},!1,!1,!1,{e:14116788e5,s:1406844e6}]},{name:"Africa/Casablanca",rules:[{e:12202236e5,s:12122784e5},{e:12508092e5,s:12438144e5},{e:1281222e6,s:12727584e5},{e:13120668e5,s:13017888e5},{e:13489704e5,s:1345428e6},{e:13828392e5,s:13761e8},{e:14142888e5,s:14069448e5}]},{name:"America/Asuncion",rules:[{e:12050316e5,s:12243888e5},{e:12364812e5,s:12558384e5},{e:12709548e5,s:12860784e5},{e:13024044e5,s:1317528e6},{e:1333854e6,s:13495824e5},{e:1364094e6,s:1381032e6},{e:13955436e5,s:14124816e5}]},{name:"America/Campo_Grande",rules:[{e:12032172e5,s:12243888e5},{e:12346668e5,s:12558384e5},{e:12667212e5,s:1287288e6},{e:12981708e5,s:13187376e5},{e:13302252e5,s:1350792e6},{e:136107e7,s:13822416e5},{e:13925196e5,s:14136912e5}]},{name:"America/Goose_Bay",rules:[{e:122559486e4,s:120503526e4},{e:125704446e4,s:123648486e4},{e:128909886e4,s:126853926e4},{e:13205556e5,s:129998886e4},{e:13520052e5,s:13314456e5},{e:13834548e5,s:13628952e5},{e:14149044e5,s:13943448e5}]},{name:"America/Havana",rules:[{e:12249972e5,s:12056436e5},{e:12564468e5,s:12364884e5},{e:12885012e5,s:12685428e5},{e:13211604e5,s:13005972e5},{e:13520052e5,s:13332564e5},{e:13834548e5,s:13628916e5},{e:14149044e5,s:13943412e5}]},{name:"America/Mazatlan",rules:[{e:1225008e6,s:12074724e5},{e:12564576e5,s:1238922e6},{e:1288512e6,s:12703716e5},{e:13199616e5,s:13018212e5},{e:13514112e5,s:13332708e5},{e:13828608e5,s:13653252e5},{e:14143104e5,s:13967748e5}]},{name:"America/Mexico_City",rules:[{e:12250044e5,s:12074688e5},{e:1256454e6,s:12389184e5},{e:12885084e5,s:1270368e6},{e:1319958e6,s:13018176e5},{e:13514076e5,s:13332672e5},{e:13828572e5,s:13653216e5},{e:14143068e5,s:13967712e5}]},{name:"America/Miquelon",rules:[{e:12255984e5,s:12050388e5},{e:1257048e6,s:12364884e5},{e:12891024e5,s:12685428e5},{e:1320552e6,s:12999924e5},{e:13520016e5,s:1331442e6},{e:13834512e5,s:13628916e5},{e:14149008e5,s:13943412e5}]},{name:"America/Santa_Isabel",rules:[{e:12250116e5,s:1207476e6},{e:12564612e5,s:12389256e5},{e:12885156e5,s:12703752e5},{e:13199652e5,s:13018248e5},{e:13514148e5,s:13332744e5},{e:13828644e5,s:13653288e5},{e:1414314e6,s:13967784e5}]},{name:"America/Santiago",rules:[{e:1206846e6,s:1223784e6},{e:1237086e6,s:12552336e5},{e:127035e7,s:12866832e5},{e:13048236e5,s:13138992e5},{e:13356684e5,s:13465584e5},{e:1367118e6,s:13786128e5},{e:13985676e5,s:14100624e5}]},{name:"America/Sao_Paulo",rules:[{e:12032136e5,s:12243852e5},{e:12346632e5,s:12558348e5},{e:12667176e5,s:12872844e5},{e:12981672e5,s:1318734e6},{e:13302216e5,s:13507884e5},{e:13610664e5,s:1382238e6},{e:1392516e6,s:14136876e5}]},{name:"Asia/Amman",rules:[{e:1225404e6,s:12066552e5},{e:12568536e5,s:12381048e5},{e:12883032e5,s:12695544e5},{e:13197528e5,s:13016088e5},!1,!1,{e:14147064e5,s:13959576e5}]},{name:"Asia/Damascus",rules:[{e:12254868e5,s:120726e7},{e:125685e7,s:12381048e5},{e:12882996e5,s:12701592e5},{e:13197492e5,s:13016088e5},{e:13511988e5,s:13330584e5},{e:13826484e5,s:1364508e6},{e:14147028e5,s:13959576e5}]},{name:"Asia/Dubai",rules:[!1,!1,!1,!1,!1,!1,!1]},{name:"Asia/Gaza",rules:[{e:12199572e5,s:12066552e5},{e:12520152e5,s:12381048e5},{e:1281474e6,s:126964086e4},{e:1312146e6,s:130160886e4},{e:13481784e5,s:13330584e5},{e:13802292e5,s:1364508e6},{e:1414098e6,s:13959576e5}]},{name:"Asia/Irkutsk",rules:[{e:12249576e5,s:12068136e5},{e:12564072e5,s:12382632e5},{e:12884616e5,s:12697128e5},!1,!1,!1,!1]},{name:"Asia/Jerusalem",rules:[{e:12231612e5,s:12066624e5},{e:1254006e6,s:1238112e6},{e:1284246e6,s:12695616e5},{e:131751e7,s:1301616e6},{e:13483548e5,s:13330656e5},{e:13828284e5,s:13645152e5},{e:1414278e6,s:13959648e5}]},{name:"Asia/Kamchatka",rules:[{e:12249432e5,s:12067992e5},{e:12563928e5,s:12382488e5},{e:12884508e5,s:12696984e5},!1,!1,!1,!1]},{name:"Asia/Krasnoyarsk",rules:[{e:12249612e5,s:12068172e5},{e:12564108e5,s:12382668e5},{e:12884652e5,s:12697164e5},!1,!1,!1,!1]},{name:"Asia/Omsk",rules:[{e:12249648e5,s:12068208e5},{e:12564144e5,s:12382704e5},{e:12884688e5,s:126972e7},!1,!1,!1,!1]},{name:"Asia/Vladivostok",rules:[{e:12249504e5,s:12068064e5},{e:12564e8,s:1238256e6},{e:12884544e5,s:12697056e5},!1,!1,!1,!1]},{name:"Asia/Yakutsk",rules:[{e:1224954e6,s:120681e7},{e:12564036e5,s:12382596e5},{e:1288458e6,s:12697092e5},!1,!1,!1,!1]},{name:"Asia/Yekaterinburg",rules:[{e:12249684e5,s:12068244e5},{e:1256418e6,s:1238274e6},{e:12884724e5,s:12697236e5},!1,!1,!1,!1]},{name:"Asia/Yerevan",rules:[{e:1224972e6,s:1206828e6},{e:12564216e5,s:12382776e5},{e:1288476e6,s:12697272e5},{e:13199256e5,s:13011768e5},!1,!1,!1]},{name:"Australia/Lord_Howe",rules:[{e:12074076e5,s:12231342e5},{e:12388572e5,s:12545838e5},{e:12703068e5,s:12860334e5},{e:13017564e5,s:1317483e6},{e:1333206e6,s:13495374e5},{e:13652604e5,s:1380987e6},{e:139671e7,s:14124366e5}]},{name:"Australia/Perth",rules:[{e:12068136e5,s:12249576e5},!1,!1,!1,!1,!1,!1]},{name:"Europe/Helsinki",rules:[{e:12249828e5,s:12068388e5},{e:12564324e5,s:12382884e5},{e:12884868e5,s:1269738e6},{e:13199364e5,s:13011876e5},{e:1351386e6,s:13326372e5},{e:13828356e5,s:13646916e5},{e:14142852e5,s:13961412e5}]},{name:"Europe/Minsk",rules:[{e:12249792e5,s:12068352e5},{e:12564288e5,s:12382848e5},{e:12884832e5,s:12697344e5},!1,!1,!1,!1]},{name:"Europe/Moscow",rules:[{e:12249756e5,s:12068316e5},{e:12564252e5,s:12382812e5},{e:12884796e5,s:12697308e5},!1,!1,!1,!1]},{name:"Pacific/Apia",rules:[!1,!1,!1,{e:13017528e5,s:13168728e5},{e:13332024e5,s:13489272e5},{e:13652568e5,s:13803768e5},{e:13967064e5,s:14118264e5}]},{name:"Pacific/Fiji",rules:[!1,!1,{e:12696984e5,s:12878424e5},{e:13271544e5,s:1319292e6},{e:1358604e6,s:13507416e5},{e:139005e7,s:1382796e6},{e:14215032e5,s:14148504e5}]},{name:"Europe/London",rules:[{e:12249828e5,s:12068388e5},{e:12564324e5,s:12382884e5},{e:12884868e5,s:1269738e6},{e:13199364e5,s:13011876e5},{e:1351386e6,s:13326372e5},{e:13828356e5,s:13646916e5},{e:14142852e5,s:13961412e5}]}]},"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=a:"undefined"!=typeof define&&null!==define&&null!=define.amd?define([],function(){return a}):"undefined"==typeof e?window.jstz=a:e.jstz=a}(); 

\ No newline at end of file

@@ -0,0 +1,1 @@ 

+ jstz-1.0.6.js 

\ No newline at end of file

@@ -0,0 +1,1 @@ 

+ jstz-1.0.6.min.js 

\ No newline at end of file

@@ -272,6 +272,8 @@ 

  <script type="text/javascript" src="{{

    url_for('static', filename='vendor/d3/d3.v3.min.js') }}"></script>

  <script type="text/javascript" src="{{

+   url_for('static', filename='vendor/jstimezonedetect/jstz.min.js') }}"></script>

+ <script type="text/javascript" src="{{

    url_for('static', filename='vendor/cal-heatmap/cal-heatmap.min.js') }}"></script>

  <link rel="stylesheet" href="{{

    url_for('static', filename='vendor/cal-heatmap/cal-heatmap.css') }}" />
@@ -305,8 +307,7 @@ 

          $('#user-activity').hide();

        });

        var cal = new CalHeatMap();

-       var offset = new Date().getTimezoneOffset();

-       offset = -offset;

+       var tz = jstz.determine().name();

        cal.init({

          cellSize: 9,

          domain: "month",
@@ -315,7 +316,7 @@ 

          start: new Date(new Date().setMonth(new Date().getMonth() - 11)),

          data: "{{ url_for(

            'api_ns.api_view_user_activity_stats',

-           username=username, format='timestamp') }}" + '&offset=' + offset,

+           username=username, format='timestamp') }}" + '&tz=' + tz,

          dataType: "json",

          highlight: "now",

          onClick: function(date, nb) {
@@ -325,7 +326,7 @@ 

              type: 'GET',

              url: "{{ url_for(

                'api_ns.api_view_user_activity_date',

-               username=username, date='') }}" + date + '?grouped=1&offset=' + offset,

+               username=username, date='') }}" + date + '?grouped=1&tz=' + tz,

              contentType: "application/json",

              dataType: 'json',

              success: function(data) {

@@ -470,16 +470,21 @@ 

  

      @patch('pagure.lib.notify.send_email')

      def test_api_view_user_activity_timezone_negative(self, mockemail):

-         """Test api_view_user_activity{_stats,_date} with a timezone

-         5 hours behind UTC. The activities will occur on 2018-02-15 in

-         UTC, but on 2018-02-14 in local time.

+         """Test api_view_user_activity{_stats,_date} with the America/

+         New York timezone, which is 5 hours behind UTC in winter and

+         4 hours behind UTC in summer (daylight savings). The events

+         will occur on 2018-02-15 in UTC, but on 2018-02-14 local.

          """

          tests.create_projects(self.session)

          repo = pagure.lib._get_project(self.session, 'test')

  

          dateobj = datetime.datetime(2018, 2, 15, 3, 30)

          utcdate = '2018-02-15'

+         # the Unix timestamp for 2018-02-15 12:00 UTC

+         utcts = '1518696000'

          localdate = '2018-02-14'

+         # the Unix timestamp for 2018-02-14 12:00 America/New_York

+         localts = '1518627600'

          # Create a single commit log

          log = model.PagureLog(

              user_id=1,
@@ -493,21 +498,33 @@ 

          self.session.add(log)

          self.session.commit()

  

-         # Retrieve the user's stats with no offset

+         # Retrieve the user's stats with no timezone specified (==UTC)

          output = self.app.get('/api/0/user/pingou/activity/stats')

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.data)

          # date in output should be UTC date

          self.assertDictEqual(data, {utcdate: 1})

+         # Now in timestamp format...

+         output = self.app.get('/api/0/user/pingou/activity/stats?format=timestamp')

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.data)

+         # timestamp in output should be UTC ts

+         self.assertDictEqual(data, {utcts: 1})

  

-         # Retrieve the user's stats with correct offset

-         output = self.app.get('/api/0/user/pingou/activity/stats?offset=-300')

+         # Retrieve the user's stats with local timezone specified

+         output = self.app.get('/api/0/user/pingou/activity/stats?tz=America/New_York')

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.data)

          # date in output should be local date

          self.assertDictEqual(data, {localdate: 1})

+         # Now in timestamp format...

+         output = self.app.get('/api/0/user/pingou/activity/stats?format=timestamp&tz=America/New_York')

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.data)

+         # timestamp in output should be local ts

+         self.assertDictEqual(data, {localts: 1})

  

-         # Retrieve the user's logs for 2018-02-15 with no offset

+         # Retrieve the user's logs for 2018-02-15 with no timezone

          output = self.app.get(

              '/api/0/user/pingou/activity/%s?grouped=1' % utcdate)

          self.assertEqual(output.status_code, 200)
@@ -522,10 +539,9 @@ 

          }

          self.assertEqual(data, exp)

  

-         # Now retrieve the user's logs for 2018-02-14 with correct

-         # offset applied

+         # Now retrieve the user's logs for 2018-02-14 with local time

          output = self.app.get(

-             '/api/0/user/pingou/activity/%s?grouped=1&offset=-300' % localdate)

+             '/api/0/user/pingou/activity/%s?grouped=1&tz=America/New_York' % localdate)

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.data)

          exp['date'] = localdate
@@ -533,16 +549,20 @@ 

  

      @patch('pagure.lib.notify.send_email')

      def test_api_view_user_activity_timezone_positive(self, mockemail):

-         """Test api_view_user_activity{_stats,_date} with a timezone

-         4 hours ahead of UTC. The activities will occur on 2018-02-15

-         in UTC, but on 2018-02-16 in local time.

+         """Test api_view_user_activity{_stats,_date} with the Asia/

+         Dubai timezone, which is 4 hours ahead of UTC. The events will

+         occur on 2018-02-15 in UTC, but on 2018-02-16 in local time.

          """

          tests.create_projects(self.session)

          repo = pagure.lib._get_project(self.session, 'test')

  

          dateobj = datetime.datetime(2018, 2, 15, 22, 30)

          utcdate = '2018-02-15'

+         # the Unix timestamp for 2018-02-15 12:00 UTC

+         utcts = '1518696000'

          localdate = '2018-02-16'

+         # the Unix timestamp for 2018-02-16 12:00 Asia/Dubai

+         localts = '1518768000'

          # Create a single commit log

          log = model.PagureLog(

              user_id=1,
@@ -556,21 +576,33 @@ 

          self.session.add(log)

          self.session.commit()

  

-         # Retrieve the user's stats with no offset

+         # Retrieve the user's stats with no timezone specified (==UTC)

          output = self.app.get('/api/0/user/pingou/activity/stats')

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.data)

          # date in output should be UTC date

          self.assertDictEqual(data, {utcdate: 1})

+         # Now in timestamp format...

+         output = self.app.get('/api/0/user/pingou/activity/stats?format=timestamp')

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.data)

+         # timestamp in output should be UTC ts

+         self.assertDictEqual(data, {utcts: 1})

  

-         # Retrieve the user's stats with correct offset

-         output = self.app.get('/api/0/user/pingou/activity/stats?offset=240')

+         # Retrieve the user's stats with local timezone specified

+         output = self.app.get('/api/0/user/pingou/activity/stats?tz=Asia/Dubai')

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.data)

          # date in output should be local date

          self.assertDictEqual(data, {localdate: 1})

+         # Now in timestamp format...

+         output = self.app.get('/api/0/user/pingou/activity/stats?format=timestamp&tz=Asia/Dubai')

+         self.assertEqual(output.status_code, 200)

+         data = json.loads(output.data)

+         # timestamp in output should be local ts

+         self.assertDictEqual(data, {localts: 1})

  

-         # Retrieve the user's logs for 2018-02-15 with no offset

+         # Retrieve the user's logs for 2018-02-15 with no timezone

          output = self.app.get(

              '/api/0/user/pingou/activity/%s?grouped=1' % utcdate)

          self.assertEqual(output.status_code, 200)
@@ -585,10 +617,9 @@ 

          }

          self.assertEqual(data, exp)

  

-         # Now retrieve the user's logs for 2018-02-16 with correct

-         # offset applied

+         # Now retrieve the user's logs for 2018-02-16 with local time

          output = self.app.get(

-             '/api/0/user/pingou/activity/%s?grouped=1&offset=240' % localdate)

+             '/api/0/user/pingou/activity/%s?grouped=1&tz=Asia/Dubai' % localdate)

          self.assertEqual(output.status_code, 200)

          data = json.loads(output.data)

          exp['date'] = localdate

My previous attempt (in f99ac7c) still had two clear problems.
Using the current offset from UTC for the local timezone
isn't really good enough: for timezones that have daylight
savings, for instance, it'll be wrong for events that happened
in the other state (so, events that happened during daylight
savings when the query is run outside of daylight savings,
for instance).

Also, the heatmap could still be wrong, because while we now
always had the right target date in mind, we were not smart
enough about making sure we fed cal-heatmap a timestamp that
definitely fell on that date in the local timezone.

This should fix both problems. Unfortunately, we need a new JS
library to do it. Getting the actual timezone (as opposed to
the offset) is a bit tricky; it is possible to get it from many
newer browsers via the Internationalization API, but some still
do not support this, so best practice is to use a library which
takes that value if possible, but otherwise tries to figure out
the timezone by requesting the offset at various points in time
and inferring from the reported values.

We change PagureLog.date_offset() from the previous attempt to
PagureLog.date_tz(), expecting a timezone name (Olson format),
and use it much as before. To solve the heatmap issue, we try
to get 12:00 on the target date in the local timezone, and
convert that to a timestamp.

Signed-off-by: Adam Williamson awilliam@redhat.com

Note, another thing we could do here is allow the user to set their preferred timezone. The default value for all user accounts could be whatever we get from jstimezonedetect, but users could override it in their account preferences. I just didn't feel like putting all that together, though. :)

I haven't looked around for other places where we might want to consider the user's local time, either. If anyone knows of any, poke me and I can have a look.

Oh, hey, there's one: when you hover over the relative times (like "Proposed an hour ago") it shows you an absolute time that's UTC (but isn't labelled as such). It should at least be labelled as UTC, and we should probably convert it to local time...(note: github shows it in local time).

Looks fine to me, thanks for working on this! :)

Commit 8a161a9 fixes this pull-request

Pull-Request has been merged by pingou

6 years ago

Oh, btw, I don't know what your workflow for adding JS libraries is, so I just manually added this one in the same form as the existing ones - I did the file naming and symlink creation manually.